@cdot65/prisma-airs-cli 5.0.1 → 5.1.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 +4 -0
- package/dist/{chunk-IRLIFMGQ.js → chunk-DR6MZG4E.js} +458 -60
- package/dist/cli/index.js +105 -31
- package/dist/index.d.ts +41 -2
- package/dist/index.js +7 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
- **Runtime Scanning** — scan prompts and responses against AIRS security profiles, single or bulk with CSV export
|
|
17
17
|
- **Daily environment report** — `airs runtime report` delivers a read-only AI Runtime Security dashboard as self-contained HTML (default) or Markdown in your working directory, with explicit evidence gaps and configuration review findings
|
|
18
|
+
- **Red Team environment dashboard** — `airs redteam dashboard` collects seven read-only SDK feeds into private HTML/Markdown deliverables, with source completeness, quota and risk review findings, and independently scoped scan-creation activity
|
|
18
19
|
- **SCM dashboard and sessions** — `runtime dashboard` and `runtime sessions` expose application activity, daily trends, checked pagination and explicit session-to-content drill-down. Legacy `scan-logs query` is **broken/under refactor** and exits with migration guidance; see the [session reference](https://cdot65.github.io/prisma-airs-cli/cli/runtime/sessions/)
|
|
19
20
|
- **Guardrail Optimization** — atomic CLI commands (`create`, `apply`, `eval`, `revert`) for custom topic guardrails, designed for autonomous agent loops (see [`AGENTS.md`](AGENTS.md))
|
|
20
21
|
- **AI Red Teaming** — adversarial scanning with static, dynamic, and custom prompt set attack modes
|
|
@@ -65,6 +66,9 @@ airs runtime topics revert --profile my-profile --name "Explosives"
|
|
|
65
66
|
airs redteam scan --target <uuid> --name "Full Scan" --type STATIC
|
|
66
67
|
airs redteam report <job-id>
|
|
67
68
|
|
|
69
|
+
# Read-only environment dashboard (HTML by default; --output markdown also supported)
|
|
70
|
+
airs redteam dashboard --strict
|
|
71
|
+
|
|
68
72
|
# Red team custom target adapters
|
|
69
73
|
airs redteam adapter list --output json
|
|
70
74
|
|
|
@@ -2501,7 +2501,302 @@ async function writeReportFile(path2, content) {
|
|
|
2501
2501
|
}
|
|
2502
2502
|
}
|
|
2503
2503
|
|
|
2504
|
-
// src/reports/
|
|
2504
|
+
// src/reports/redteam.ts
|
|
2505
|
+
var DataError = class extends Error {
|
|
2506
|
+
};
|
|
2507
|
+
var label = (value) => typeof value === "string" ? value.replace(/[\p{Cc}\p{Cf}]/gu, " ").slice(0, 120) : "Unknown";
|
|
2508
|
+
function count(value) {
|
|
2509
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
2510
|
+
throw new DataError("A required counter is missing or invalid.");
|
|
2511
|
+
return value;
|
|
2512
|
+
}
|
|
2513
|
+
function failure(error) {
|
|
2514
|
+
if (error instanceof DataError) return error.message;
|
|
2515
|
+
const code = error?.statusCode;
|
|
2516
|
+
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.";
|
|
2517
|
+
}
|
|
2518
|
+
async function collectRedTeamEnvironmentReport(client, options = {}) {
|
|
2519
|
+
const maxPages = options.maxPages ?? 40;
|
|
2520
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 100)
|
|
2521
|
+
throw new Error("maxPages must be an integer from 1 to 100");
|
|
2522
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2523
|
+
const end = now();
|
|
2524
|
+
const report = {
|
|
2525
|
+
schemaVersion: 1,
|
|
2526
|
+
product: "Prisma AIRS AI Red Teaming",
|
|
2527
|
+
title: options.title ?? "Red Team environment report",
|
|
2528
|
+
collectionStartedAt: end.toISOString(),
|
|
2529
|
+
generatedAt: "",
|
|
2530
|
+
window: { start: new Date(end.getTime() - 864e5).toISOString(), end: end.toISOString() },
|
|
2531
|
+
health: "unknown",
|
|
2532
|
+
sources: [],
|
|
2533
|
+
findings: [],
|
|
2534
|
+
tables: [],
|
|
2535
|
+
limitations: [
|
|
2536
|
+
"This is a read-only snapshot, not a security certification, availability SLA or numerical health score.",
|
|
2537
|
+
"Only scan creation timestamps are filtered locally to the preceding 24 hours. Current scan status is not a history of status changes.",
|
|
2538
|
+
"Dashboard statistics use server defaults with an unverified measurement window. They are not daily totals and need not match the paginated scan inventory.",
|
|
2539
|
+
"Targets and adapters are current configuration inventories, not configuration change logs. Broker channels not online may be intentionally idle; no outage is inferred.",
|
|
2540
|
+
"Risk categories are server-reported. Quota consumption is not reset or reinterpreted as daily usage; unlimited zero allocation is not exhaustion.",
|
|
2541
|
+
"Partial sources describe collected records only. Missing data is unknown, never zero. Independently read pages are not an atomic snapshot.",
|
|
2542
|
+
"Only aggregate counters and category labels are included. Target names, IDs, connection settings, scripts, variables, prompts and scan content are excluded."
|
|
2543
|
+
]
|
|
2544
|
+
};
|
|
2545
|
+
function finding(priority, title, evidence, recommendation, source3) {
|
|
2546
|
+
report.findings.push({ priority, title, evidence, recommendation, source: source3 });
|
|
2547
|
+
}
|
|
2548
|
+
async function source2(name, method, window, read) {
|
|
2549
|
+
const s = {
|
|
2550
|
+
name,
|
|
2551
|
+
method,
|
|
2552
|
+
window,
|
|
2553
|
+
status: "unavailable",
|
|
2554
|
+
records: 0,
|
|
2555
|
+
pages: 0,
|
|
2556
|
+
notes: []
|
|
2557
|
+
};
|
|
2558
|
+
report.sources.push(s);
|
|
2559
|
+
try {
|
|
2560
|
+
await read(s);
|
|
2561
|
+
if (s.status !== "partial") s.status = "complete";
|
|
2562
|
+
} catch (error) {
|
|
2563
|
+
s.status = s.records > 0 ? "partial" : "unavailable";
|
|
2564
|
+
s.notes.push(failure(error));
|
|
2565
|
+
}
|
|
2566
|
+
if (s.status !== "complete")
|
|
2567
|
+
finding(
|
|
2568
|
+
"review",
|
|
2569
|
+
`${name} incomplete`,
|
|
2570
|
+
s.notes.join(" "),
|
|
2571
|
+
"Resolve the source error or increase the page budget, then regenerate.",
|
|
2572
|
+
name
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
async function pages(s, read) {
|
|
2576
|
+
const rows = [];
|
|
2577
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2578
|
+
let total2;
|
|
2579
|
+
try {
|
|
2580
|
+
for (let page = 0; page < maxPages; page++) {
|
|
2581
|
+
const r = await read(rows.length);
|
|
2582
|
+
s.pages++;
|
|
2583
|
+
if (!Array.isArray(r.data)) throw new DataError("Missing inventory array.");
|
|
2584
|
+
const reported = count(r.pagination.total_items);
|
|
2585
|
+
total2 ??= reported;
|
|
2586
|
+
if (total2 !== reported) throw new DataError("Inventory changed during pagination.");
|
|
2587
|
+
if (r.data.length > 15) throw new DataError("Page exceeds requested size.");
|
|
2588
|
+
for (const row of r.data) {
|
|
2589
|
+
if (!row.uuid || seen.has(row.uuid))
|
|
2590
|
+
throw new DataError("Missing or repeated identity; pagination stopped.");
|
|
2591
|
+
seen.add(row.uuid);
|
|
2592
|
+
rows.push(row);
|
|
2593
|
+
s.records = rows.length;
|
|
2594
|
+
}
|
|
2595
|
+
if (rows.length > total2) throw new DataError("Inventory exceeds reported total.");
|
|
2596
|
+
if (rows.length === total2) return rows;
|
|
2597
|
+
if (!r.data.length) throw new DataError("Premature empty page.");
|
|
2598
|
+
}
|
|
2599
|
+
throw new DataError(`Reached the ${maxPages}-page budget; inventory is incomplete.`);
|
|
2600
|
+
} catch (error) {
|
|
2601
|
+
if (!rows.length) throw error;
|
|
2602
|
+
s.status = "partial";
|
|
2603
|
+
s.notes.push(failure(error));
|
|
2604
|
+
return rows;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
const table = (title, note, headers, rows) => report.tables.push({ title, note, headers, rows });
|
|
2608
|
+
function groups(values) {
|
|
2609
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2610
|
+
for (const v of values) {
|
|
2611
|
+
const key = label(v);
|
|
2612
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2613
|
+
}
|
|
2614
|
+
return [...counts].sort(([a], [b]) => a.localeCompare(b));
|
|
2615
|
+
}
|
|
2616
|
+
await source2("Overview", "getDashboardOverview()", "Current dashboard snapshot", async (s) => {
|
|
2617
|
+
const r = await client.getDashboardOverview();
|
|
2618
|
+
const total2 = count(r.total_targets);
|
|
2619
|
+
table("Dashboard overview", s.window, ["Metric", "Count"], [["Targets", total2]]);
|
|
2620
|
+
s.records = 1;
|
|
2621
|
+
s.pages = 1;
|
|
2622
|
+
});
|
|
2623
|
+
await source2(
|
|
2624
|
+
"Statistics",
|
|
2625
|
+
"getScanStatistics()",
|
|
2626
|
+
"Server-default window (unverified)",
|
|
2627
|
+
async (s) => {
|
|
2628
|
+
const r = await client.getScanStatistics();
|
|
2629
|
+
const rows = [
|
|
2630
|
+
["Scans", count(r.total_scans)],
|
|
2631
|
+
["Targets scanned", count(r.targets_scanned)]
|
|
2632
|
+
];
|
|
2633
|
+
for (const risk of r.risk_profile ?? []) {
|
|
2634
|
+
const n = count(risk.total);
|
|
2635
|
+
rows.push([`Risk: ${label(risk.risk_rating)}`, n]);
|
|
2636
|
+
if (/^(high|critical)$/i.test(risk.risk_rating) && n > 0)
|
|
2637
|
+
finding(
|
|
2638
|
+
"attention",
|
|
2639
|
+
"Elevated risk reported",
|
|
2640
|
+
`${n} in the ${label(risk.risk_rating)} risk category.`,
|
|
2641
|
+
"Review affected targets in Red Team; the statistics window is server-defined.",
|
|
2642
|
+
s.name
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
for (const status of r.scan_status ?? [])
|
|
2646
|
+
rows.push([`Status: ${label(status.name)}`, count(status.count)]);
|
|
2647
|
+
table("Dashboard scan statistics", s.window, ["Metric", "Count"], rows);
|
|
2648
|
+
s.records = 1;
|
|
2649
|
+
s.pages = 1;
|
|
2650
|
+
}
|
|
2651
|
+
);
|
|
2652
|
+
await source2("Targets", "targets.list()", "Current paginated inventory", async (s) => {
|
|
2653
|
+
const rows = await pages(s, (skip) => client.targets.list({ skip, limit: 15 }));
|
|
2654
|
+
table(
|
|
2655
|
+
"Target inventory",
|
|
2656
|
+
"Collected targets by type; consult source completeness.",
|
|
2657
|
+
["Type", "Count"],
|
|
2658
|
+
groups(rows.map((r) => r.target_type))
|
|
2659
|
+
);
|
|
2660
|
+
});
|
|
2661
|
+
await source2("Quota", "getQuotaSummary() \u2014 GET", "Current quota snapshot", async (s) => {
|
|
2662
|
+
if (!client.getQuotaSummary)
|
|
2663
|
+
throw new DataError(
|
|
2664
|
+
"Installed SDK lacks getQuotaSummary(); use the locally updated SDK. POST is not used as a fallback."
|
|
2665
|
+
);
|
|
2666
|
+
const r = await client.getQuotaSummary();
|
|
2667
|
+
const rows = [];
|
|
2668
|
+
for (const kind of ["static", "dynamic", "custom"]) {
|
|
2669
|
+
const q = r[kind];
|
|
2670
|
+
const allocated = count(q.allocated);
|
|
2671
|
+
const consumed = count(q.consumed);
|
|
2672
|
+
if (typeof q.unlimited !== "boolean") throw new DataError("Missing unlimited quota flag.");
|
|
2673
|
+
rows.push([
|
|
2674
|
+
kind,
|
|
2675
|
+
allocated,
|
|
2676
|
+
consumed,
|
|
2677
|
+
q.unlimited ? "Unlimited" : String(Math.max(0, allocated - consumed))
|
|
2678
|
+
]);
|
|
2679
|
+
if (!q.unlimited && consumed >= allocated)
|
|
2680
|
+
finding(
|
|
2681
|
+
"attention",
|
|
2682
|
+
`${kind} quota has no remaining allocation`,
|
|
2683
|
+
`${consumed} consumed; ${allocated} allocated.`,
|
|
2684
|
+
"Review quota allocation before scheduling more scans.",
|
|
2685
|
+
s.name
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
table(
|
|
2689
|
+
"Quota",
|
|
2690
|
+
"Remaining is shown only for finite quotas; these are not daily usage counters.",
|
|
2691
|
+
["Type", "Allocated", "Consumed", "Remaining"],
|
|
2692
|
+
rows
|
|
2693
|
+
);
|
|
2694
|
+
s.records = 3;
|
|
2695
|
+
s.pages = 1;
|
|
2696
|
+
});
|
|
2697
|
+
await source2(
|
|
2698
|
+
"Scans",
|
|
2699
|
+
"scans.list()",
|
|
2700
|
+
"Paginated inventory; local 24-hour creation filter",
|
|
2701
|
+
async (s) => {
|
|
2702
|
+
const rows = await pages(s, (skip) => client.scans.list({ skip, limit: 15 }));
|
|
2703
|
+
let unknown = 0;
|
|
2704
|
+
const recent = rows.filter((r) => {
|
|
2705
|
+
const t = typeof r.created_at === "string" && /(?:Z|[+-]\d{2}:\d{2})$/i.test(r.created_at) ? Date.parse(r.created_at) : Number.NaN;
|
|
2706
|
+
if (!Number.isFinite(t) || t > end.getTime()) {
|
|
2707
|
+
unknown++;
|
|
2708
|
+
return false;
|
|
2709
|
+
}
|
|
2710
|
+
return t >= end.getTime() - 864e5;
|
|
2711
|
+
});
|
|
2712
|
+
table(
|
|
2713
|
+
"Scan inventory and daily creation activity",
|
|
2714
|
+
"Daily count covers collected records created in the stated UTC window, not scans completed or attacks executed.",
|
|
2715
|
+
["Metric", "Count"],
|
|
2716
|
+
[
|
|
2717
|
+
["Collected scans", rows.length],
|
|
2718
|
+
["Created in preceding 24 hours", recent.length],
|
|
2719
|
+
["Unusable or future creation timestamps", unknown]
|
|
2720
|
+
]
|
|
2721
|
+
);
|
|
2722
|
+
table(
|
|
2723
|
+
"Current status of collected scans",
|
|
2724
|
+
"All collected scans, independent of creation window.",
|
|
2725
|
+
["Status", "Count"],
|
|
2726
|
+
groups(rows.map((r) => r.status))
|
|
2727
|
+
);
|
|
2728
|
+
const failed = rows.filter((r) => /^(failed|error)$/i.test(r.status ?? "")).length;
|
|
2729
|
+
if (failed)
|
|
2730
|
+
finding(
|
|
2731
|
+
"attention",
|
|
2732
|
+
"Scans require failure review",
|
|
2733
|
+
`${failed} collected scans currently report failed/error status.`,
|
|
2734
|
+
"Inspect individual scan reports and error logs before rerunning; no reruns are performed here.",
|
|
2735
|
+
s.name
|
|
2736
|
+
);
|
|
2737
|
+
if (unknown)
|
|
2738
|
+
finding(
|
|
2739
|
+
"review",
|
|
2740
|
+
"Daily activity has timestamp gaps",
|
|
2741
|
+
`${unknown} records could not be assigned to the daily window.`,
|
|
2742
|
+
"Review source timestamps; do not interpret the daily count as complete.",
|
|
2743
|
+
s.name
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
);
|
|
2747
|
+
await source2(
|
|
2748
|
+
"Network broker",
|
|
2749
|
+
"networkBroker.getChannelStats()",
|
|
2750
|
+
"Current channel snapshot",
|
|
2751
|
+
async (s) => {
|
|
2752
|
+
const r = await client.networkBroker.getChannelStats();
|
|
2753
|
+
const total2 = count(r.total_channels);
|
|
2754
|
+
const online = count(r.online_channels);
|
|
2755
|
+
if (online > total2) throw new DataError("Online channel count exceeds total.");
|
|
2756
|
+
table(
|
|
2757
|
+
"Network broker",
|
|
2758
|
+
"Not-online channels may be intentionally idle; this does not establish an outage.",
|
|
2759
|
+
["Metric", "Count"],
|
|
2760
|
+
[
|
|
2761
|
+
["Configured channels", total2],
|
|
2762
|
+
["Online channels", online]
|
|
2763
|
+
]
|
|
2764
|
+
);
|
|
2765
|
+
if (total2 > online)
|
|
2766
|
+
finding(
|
|
2767
|
+
"review",
|
|
2768
|
+
"Confirm expected broker connectivity",
|
|
2769
|
+
`${online} of ${total2} configured channels are online.`,
|
|
2770
|
+
"Compare with intended channel schedules before investigating connectivity.",
|
|
2771
|
+
s.name
|
|
2772
|
+
);
|
|
2773
|
+
s.records = 1;
|
|
2774
|
+
s.pages = 1;
|
|
2775
|
+
}
|
|
2776
|
+
);
|
|
2777
|
+
await source2(
|
|
2778
|
+
"Adapters",
|
|
2779
|
+
"adapters.list({ include_target_count: true })",
|
|
2780
|
+
"Current paginated configuration",
|
|
2781
|
+
async (s) => {
|
|
2782
|
+
const rows = await pages(
|
|
2783
|
+
s,
|
|
2784
|
+
(skip) => client.adapters.list({ skip, limit: 15, include_target_count: true })
|
|
2785
|
+
);
|
|
2786
|
+
table(
|
|
2787
|
+
"Adapter inventory",
|
|
2788
|
+
"Configuration status only; scripts and variables are excluded.",
|
|
2789
|
+
["Status", "Count"],
|
|
2790
|
+
groups(rows.map((r) => r.status))
|
|
2791
|
+
);
|
|
2792
|
+
}
|
|
2793
|
+
);
|
|
2794
|
+
report.health = report.sources.every((s) => s.status === "unavailable") ? "unknown" : report.findings.some((f) => f.priority === "attention") ? "attention" : report.findings.length ? "review" : "no-findings";
|
|
2795
|
+
report.generatedAt = now().toISOString();
|
|
2796
|
+
return report;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// src/reports/redteam-render.ts
|
|
2505
2800
|
import { createHash } from "crypto";
|
|
2506
2801
|
var HEALTH = {
|
|
2507
2802
|
attention: "Attention required",
|
|
@@ -2509,7 +2804,107 @@ var HEALTH = {
|
|
|
2509
2804
|
"no-findings": "No findings in available evidence",
|
|
2510
2805
|
unknown: "Unable to assess"
|
|
2511
2806
|
};
|
|
2512
|
-
|
|
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");
|
|
2883
|
+
}
|
|
2884
|
+
var CSS = `
|
|
2885
|
+
: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}}
|
|
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
|
+
`;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// src/reports/render.ts
|
|
2900
|
+
import { createHash as createHash2 } from "crypto";
|
|
2901
|
+
var HEALTH2 = {
|
|
2902
|
+
attention: "Attention required",
|
|
2903
|
+
review: "Review required",
|
|
2904
|
+
"no-findings": "No findings in available evidence",
|
|
2905
|
+
unknown: "Unable to assess"
|
|
2906
|
+
};
|
|
2907
|
+
function html2(value) {
|
|
2513
2908
|
return String(value ?? "Unknown").replace(
|
|
2514
2909
|
/[&<>"']/g,
|
|
2515
2910
|
(char) => ({
|
|
@@ -2521,7 +2916,7 @@ function html(value) {
|
|
|
2521
2916
|
})[char] ?? char
|
|
2522
2917
|
);
|
|
2523
2918
|
}
|
|
2524
|
-
function
|
|
2919
|
+
function md2(value) {
|
|
2525
2920
|
return String(value ?? "Unknown").replace(
|
|
2526
2921
|
/[&<>"'\\`*_[\]{}()|!#~\r\n]/g,
|
|
2527
2922
|
(char) => char === "\r" || char === "\n" ? " " : `&#${char.charCodeAt(0)};`
|
|
@@ -2530,7 +2925,7 @@ function md(value) {
|
|
|
2530
2925
|
function number(value) {
|
|
2531
2926
|
return value === null ? "Unknown" : value.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
2532
2927
|
}
|
|
2533
|
-
function
|
|
2928
|
+
function tables2(report) {
|
|
2534
2929
|
const sessionSource = report.sources.find((source2) => source2.name === "Daily session inventory");
|
|
2535
2930
|
return [
|
|
2536
2931
|
{
|
|
@@ -2660,15 +3055,15 @@ function metrics(report) {
|
|
|
2660
3055
|
}
|
|
2661
3056
|
function renderRuntimeReportMarkdown(report) {
|
|
2662
3057
|
const lines = [
|
|
2663
|
-
`# ${
|
|
3058
|
+
`# ${md2(report.title)}`,
|
|
2664
3059
|
"",
|
|
2665
|
-
`**${
|
|
3060
|
+
`**${md2(report.product)}**`,
|
|
2666
3061
|
"",
|
|
2667
|
-
`Assessment: **${
|
|
3062
|
+
`Assessment: **${HEALTH2[report.health]}**`,
|
|
2668
3063
|
"",
|
|
2669
|
-
`Window: ${
|
|
3064
|
+
`Window: ${md2(report.window.start)} \u2192 ${md2(report.window.end)}`,
|
|
2670
3065
|
"",
|
|
2671
|
-
`${
|
|
3066
|
+
`${md2(report.window.description)}. Collection: ${md2(report.collectionStartedAt)} \u2192 ${md2(report.generatedAt)}.`,
|
|
2672
3067
|
"",
|
|
2673
3068
|
"## At a glance",
|
|
2674
3069
|
"",
|
|
@@ -2684,17 +3079,17 @@ function renderRuntimeReportMarkdown(report) {
|
|
|
2684
3079
|
);
|
|
2685
3080
|
for (const finding of report.findings)
|
|
2686
3081
|
lines.push(
|
|
2687
|
-
`### ${finding.priority.toUpperCase()}: ${
|
|
3082
|
+
`### ${finding.priority.toUpperCase()}: ${md2(finding.title)}`,
|
|
2688
3083
|
"",
|
|
2689
|
-
`Evidence: ${
|
|
3084
|
+
`Evidence: ${md2(finding.evidence)}`,
|
|
2690
3085
|
"",
|
|
2691
|
-
`Next step: ${
|
|
3086
|
+
`Next step: ${md2(finding.recommendation)}`,
|
|
2692
3087
|
"",
|
|
2693
|
-
`Source: ${
|
|
3088
|
+
`Source: ${md2(finding.source)}`,
|
|
2694
3089
|
""
|
|
2695
3090
|
);
|
|
2696
|
-
for (const table of
|
|
2697
|
-
lines.push(`## ${table.title}`, "",
|
|
3091
|
+
for (const table of tables2(report)) {
|
|
3092
|
+
lines.push(`## ${table.title}`, "", md2(table.note), "");
|
|
2698
3093
|
if (table.rows.length === 0)
|
|
2699
3094
|
lines.push(
|
|
2700
3095
|
"No rows available. Consult the source status before interpreting this as an empty result.",
|
|
@@ -2704,21 +3099,21 @@ function renderRuntimeReportMarkdown(report) {
|
|
|
2704
3099
|
lines.push(
|
|
2705
3100
|
`| ${table.headers.join(" | ")} |`,
|
|
2706
3101
|
`| ${table.headers.map(() => "---").join(" | ")} |`,
|
|
2707
|
-
...table.rows.map((row) => `| ${row.map(
|
|
3102
|
+
...table.rows.map((row) => `| ${row.map(md2).join(" | ")} |`),
|
|
2708
3103
|
""
|
|
2709
3104
|
);
|
|
2710
3105
|
}
|
|
2711
3106
|
lines.push(
|
|
2712
3107
|
"## Scope, privacy, and limitations",
|
|
2713
3108
|
"",
|
|
2714
|
-
...report.limitations.map((note) => `- ${
|
|
3109
|
+
...report.limitations.map((note) => `- ${md2(note)}`),
|
|
2715
3110
|
"",
|
|
2716
3111
|
`Generated by Prisma AIRS CLI \xB7 Report schema ${report.schemaVersion}`,
|
|
2717
3112
|
""
|
|
2718
3113
|
);
|
|
2719
3114
|
return lines.join("\n");
|
|
2720
3115
|
}
|
|
2721
|
-
var
|
|
3116
|
+
var CSS2 = `
|
|
2722
3117
|
#evidence td:nth-child(3){white-space:nowrap}
|
|
2723
3118
|
: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
3119
|
*{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 +3121,7 @@ main{max-width:1280px;margin:auto;padding:1.5rem 1.25rem 3rem}nav{display:flex;g
|
|
|
2726
3121
|
@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
3122
|
@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
3123
|
`;
|
|
2729
|
-
var
|
|
3124
|
+
var JS2 = `
|
|
2730
3125
|
'use strict';
|
|
2731
3126
|
document.documentElement.classList.add('js-ready');
|
|
2732
3127
|
document.getElementById('print-report').addEventListener('click',function(){window.print();});
|
|
@@ -2744,21 +3139,21 @@ search.addEventListener('input',function(){
|
|
|
2744
3139
|
});
|
|
2745
3140
|
`;
|
|
2746
3141
|
function renderRuntimeReportHtml(report) {
|
|
2747
|
-
const hash = (text) =>
|
|
2748
|
-
const policy = `default-src 'none'; script-src 'sha256-${hash(
|
|
3142
|
+
const hash = (text) => createHash2("sha256").update(text).digest("base64");
|
|
3143
|
+
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
3144
|
const findingCards = report.findings.map(
|
|
2750
|
-
(finding) => `<article class="finding" data-priority="${
|
|
3145
|
+
(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
3146
|
).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">${
|
|
3147
|
+
const sections = tables2(report).map(
|
|
3148
|
+
(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
3149
|
).join("");
|
|
2755
3150
|
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="${
|
|
3151
|
+
<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>
|
|
3152
|
+
<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
3153
|
<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
3154
|
<div class="cards">${metrics(report).map(([name, value]) => `<div class="card"><span>${name}</span><strong>${value}</strong></div>`).join("")}</div>
|
|
2760
3155
|
<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>${
|
|
3156
|
+
${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
3157
|
`;
|
|
2763
3158
|
}
|
|
2764
3159
|
|
|
@@ -2778,10 +3173,10 @@ function safeFailure(error) {
|
|
|
2778
3173
|
}
|
|
2779
3174
|
return "Source request or response validation failed; check connectivity and SDK compatibility.";
|
|
2780
3175
|
}
|
|
2781
|
-
function
|
|
3176
|
+
function label2(value) {
|
|
2782
3177
|
return (value?.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim() || "Unknown").slice(0, 240);
|
|
2783
3178
|
}
|
|
2784
|
-
function
|
|
3179
|
+
function count2(value) {
|
|
2785
3180
|
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
2786
3181
|
}
|
|
2787
3182
|
async function collect(source2, maxPages, read, identity) {
|
|
@@ -2831,23 +3226,23 @@ function inventoryNext(next, size, offset) {
|
|
|
2831
3226
|
function total(values) {
|
|
2832
3227
|
if (values.some((value) => value === null)) return null;
|
|
2833
3228
|
const result = values.reduce((sum, value) => sum + (value ?? 0), 0);
|
|
2834
|
-
return
|
|
3229
|
+
return count2(result);
|
|
2835
3230
|
}
|
|
2836
3231
|
function distribution(rows) {
|
|
2837
3232
|
const groups = /* @__PURE__ */ new Map();
|
|
2838
3233
|
for (const row of rows) {
|
|
2839
|
-
const name =
|
|
3234
|
+
const name = label2(row.violation_status).toLowerCase();
|
|
2840
3235
|
groups.set(name, (groups.get(name) ?? 0) + 1);
|
|
2841
3236
|
}
|
|
2842
3237
|
return [...groups].map(([name, n]) => ({ name, count: n })).sort((a, b) => b.count - a.count);
|
|
2843
3238
|
}
|
|
2844
3239
|
function severity(value) {
|
|
2845
3240
|
return {
|
|
2846
|
-
critical:
|
|
2847
|
-
high:
|
|
2848
|
-
medium:
|
|
2849
|
-
low:
|
|
2850
|
-
total:
|
|
3241
|
+
critical: count2(value.critical),
|
|
3242
|
+
high: count2(value.high),
|
|
3243
|
+
medium: count2(value.medium),
|
|
3244
|
+
low: count2(value.low),
|
|
3245
|
+
total: count2(value.total)
|
|
2851
3246
|
};
|
|
2852
3247
|
}
|
|
2853
3248
|
async function collectRuntimeDailyReport(client, options = {}) {
|
|
@@ -2910,7 +3305,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
2910
3305
|
offset
|
|
2911
3306
|
});
|
|
2912
3307
|
const size = page.items?.length ?? 0;
|
|
2913
|
-
const totalItems =
|
|
3308
|
+
const totalItems = count2(page.pagination?.total_items);
|
|
2914
3309
|
if (totalItems !== null) {
|
|
2915
3310
|
if (expectedAppTotal !== void 0 && totalItems !== expectedAppTotal)
|
|
2916
3311
|
throw new ReportDataError(
|
|
@@ -2962,7 +3357,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
2962
3357
|
async (offset) => {
|
|
2963
3358
|
const page = await client.dashboard.sessionsOverview({ ...daily, limit: 25, offset });
|
|
2964
3359
|
const size = page.items?.length ?? 0;
|
|
2965
|
-
const totalItems =
|
|
3360
|
+
const totalItems = count2(page.pagination?.total_items);
|
|
2966
3361
|
if (totalItems === null || page.pagination?.skip !== offset || page.pagination?.limit !== 25)
|
|
2967
3362
|
throw new ReportDataError("Missing or mismatched session pagination metadata.");
|
|
2968
3363
|
if (expectedSessionTotal !== void 0 && expectedSessionTotal !== totalItems)
|
|
@@ -3018,9 +3413,9 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3018
3413
|
}
|
|
3019
3414
|
const appIds = new Set(registered.map((app) => app.customer_appId));
|
|
3020
3415
|
const reportApps = applications.map((app) => ({
|
|
3021
|
-
name:
|
|
3022
|
-
sessions:
|
|
3023
|
-
violatingSessions:
|
|
3416
|
+
name: label2(app.name),
|
|
3417
|
+
sessions: count2(app.sessions_total),
|
|
3418
|
+
violatingSessions: count2(app.sessions_violated),
|
|
3024
3419
|
registered: app.id && appIds.has(app.id) ? true : appSource.status === "complete" && app.id ? false : null
|
|
3025
3420
|
})).sort(
|
|
3026
3421
|
(a, b) => (b.violatingSessions ?? -1) - (a.violatingSessions ?? -1) || a.name.localeCompare(b.name)
|
|
@@ -3066,9 +3461,9 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3066
3461
|
(p) => p["model-configuration"] ? [p["model-configuration"]] : []
|
|
3067
3462
|
);
|
|
3068
3463
|
const timeoutActions = [
|
|
3069
|
-
...new Set(configs.map((c) =>
|
|
3464
|
+
...new Set(configs.map((c) => label2(c.latency?.["inline-timeout-action"])))
|
|
3070
3465
|
];
|
|
3071
|
-
const name =
|
|
3466
|
+
const name = label2(profile.profile_name);
|
|
3072
3467
|
if (profile.active === false)
|
|
3073
3468
|
add(
|
|
3074
3469
|
"review",
|
|
@@ -3097,7 +3492,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3097
3492
|
);
|
|
3098
3493
|
return {
|
|
3099
3494
|
name,
|
|
3100
|
-
revision:
|
|
3495
|
+
revision: count2(profile.revision),
|
|
3101
3496
|
active: profile.active ?? null,
|
|
3102
3497
|
modified: profile.last_modified_ts && Number.isFinite(Date.parse(profile.last_modified_ts)) ? new Date(profile.last_modified_ts).toISOString() : null,
|
|
3103
3498
|
timeoutActions,
|
|
@@ -3145,8 +3540,8 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3145
3540
|
"Review these sessions in SCM and check integration error handling. This is not a tenant-wide failure rate.",
|
|
3146
3541
|
sessionSource.name
|
|
3147
3542
|
);
|
|
3148
|
-
const chartSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) =>
|
|
3149
|
-
const chartViolatingSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) =>
|
|
3543
|
+
const chartSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) => count2(bucket.total)));
|
|
3544
|
+
const chartViolatingSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) => count2(bucket.violating)));
|
|
3150
3545
|
if (chartSessions !== null && sessionSource.status === "complete" && chartSessions !== rawSessions.length)
|
|
3151
3546
|
add(
|
|
3152
3547
|
"review",
|
|
@@ -3157,7 +3552,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3157
3552
|
);
|
|
3158
3553
|
const urgentViolations = total(
|
|
3159
3554
|
trend.map(
|
|
3160
|
-
(bucket) => total([
|
|
3555
|
+
(bucket) => total([count2(bucket.violation_breakdown.critical), count2(bucket.violation_breakdown.high)])
|
|
3161
3556
|
)
|
|
3162
3557
|
);
|
|
3163
3558
|
if ((urgentViolations ?? 0) > 0)
|
|
@@ -3173,7 +3568,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3173
3568
|
return {
|
|
3174
3569
|
schemaVersion: 2,
|
|
3175
3570
|
product: "Prisma AIRS AI Runtime Security",
|
|
3176
|
-
title:
|
|
3571
|
+
title: label2(options.title ?? "Daily environment report"),
|
|
3177
3572
|
generatedAt: now().toISOString(),
|
|
3178
3573
|
collectionStartedAt: end,
|
|
3179
3574
|
window: {
|
|
@@ -3192,10 +3587,10 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3192
3587
|
},
|
|
3193
3588
|
profiles,
|
|
3194
3589
|
registeredApps: registered.map((app) => ({
|
|
3195
|
-
name:
|
|
3196
|
-
environment:
|
|
3197
|
-
cloud:
|
|
3198
|
-
model:
|
|
3590
|
+
name: label2(app.app_name),
|
|
3591
|
+
environment: label2(app.environment),
|
|
3592
|
+
cloud: label2(app.cloud_provider),
|
|
3593
|
+
model: label2(app.model_name),
|
|
3199
3594
|
keyAssociations: app.api_keys_dp_info ? app.api_keys_dp_info.length : null
|
|
3200
3595
|
})).sort((a, b) => a.name.localeCompare(b.name)),
|
|
3201
3596
|
sessions: {
|
|
@@ -3210,22 +3605,22 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3210
3605
|
sessions: chartSessions,
|
|
3211
3606
|
violatingSessions: chartViolatingSessions,
|
|
3212
3607
|
buckets: chart.map((bucket) => ({
|
|
3213
|
-
time:
|
|
3214
|
-
sessions:
|
|
3215
|
-
violatingSessions:
|
|
3608
|
+
time: label2(bucket.time),
|
|
3609
|
+
sessions: count2(bucket.total),
|
|
3610
|
+
violatingSessions: count2(bucket.violating),
|
|
3216
3611
|
violations: severity(bucket.violation_breakdown)
|
|
3217
3612
|
}))
|
|
3218
3613
|
},
|
|
3219
3614
|
topApplications: rankings.map((app) => ({
|
|
3220
|
-
name:
|
|
3221
|
-
violations:
|
|
3615
|
+
name: label2(app.name),
|
|
3616
|
+
violations: count2(app.total_violations),
|
|
3222
3617
|
detectors: app.policy_violations.map((detector) => ({
|
|
3223
|
-
name:
|
|
3224
|
-
count:
|
|
3618
|
+
name: label2(detector.detection_type),
|
|
3619
|
+
count: count2(detector.total)
|
|
3225
3620
|
}))
|
|
3226
3621
|
})),
|
|
3227
3622
|
violationTrend: trend.map((bucket) => ({
|
|
3228
|
-
time:
|
|
3623
|
+
time: label2(bucket.date),
|
|
3229
3624
|
violations: severity(bucket.violation_breakdown)
|
|
3230
3625
|
}))
|
|
3231
3626
|
},
|
|
@@ -3268,6 +3663,9 @@ export {
|
|
|
3268
3663
|
computeMetrics,
|
|
3269
3664
|
computeCategoryBreakdown,
|
|
3270
3665
|
writeReportFile,
|
|
3666
|
+
collectRedTeamEnvironmentReport,
|
|
3667
|
+
renderRedTeamReportMarkdown,
|
|
3668
|
+
renderRedTeamReportHtml,
|
|
3271
3669
|
renderRuntimeReportMarkdown,
|
|
3272
3670
|
renderRuntimeReportHtml,
|
|
3273
3671
|
collectRuntimeDailyReport
|
package/dist/cli/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
SdkPromptSetService,
|
|
10
10
|
SdkRedTeamService,
|
|
11
11
|
SdkRuntimeService,
|
|
12
|
+
collectRedTeamEnvironmentReport,
|
|
12
13
|
collectRuntimeDailyReport,
|
|
13
14
|
computeMetrics,
|
|
14
15
|
getOrCreateManagementClient,
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
loadConfig,
|
|
17
18
|
readBackupDir,
|
|
18
19
|
readBackupFile,
|
|
20
|
+
renderRedTeamReportHtml,
|
|
21
|
+
renderRedTeamReportMarkdown,
|
|
19
22
|
renderRuntimeReportHtml,
|
|
20
23
|
renderRuntimeReportMarkdown,
|
|
21
24
|
resolveConfigFilePath,
|
|
@@ -24,7 +27,7 @@ import {
|
|
|
24
27
|
validateTopic,
|
|
25
28
|
writeBackupFile,
|
|
26
29
|
writeReportFile
|
|
27
|
-
} from "../chunk-
|
|
30
|
+
} from "../chunk-DR6MZG4E.js";
|
|
28
31
|
|
|
29
32
|
// src/cli/index.ts
|
|
30
33
|
import "dotenv/config";
|
|
@@ -43,7 +46,7 @@ function installProcessGuards() {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
// src/cli/program.ts
|
|
46
|
-
import { randomUUID as
|
|
49
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
47
50
|
import { readFileSync as readFileSync4 } from "fs";
|
|
48
51
|
import { dirname as dirname4, join as join4 } from "path";
|
|
49
52
|
import { fileURLToPath } from "url";
|
|
@@ -3052,8 +3055,8 @@ function registerAiGatewayInference(parent) {
|
|
|
3052
3055
|
process.once("SIGINT", interrupt);
|
|
3053
3056
|
process.once("SIGTERM", interrupt);
|
|
3054
3057
|
process.stdout.on("error", outputError);
|
|
3055
|
-
const write = (text2) => new Promise((
|
|
3056
|
-
process.stdout.write(text2, (error) => error ? reject(error) :
|
|
3058
|
+
const write = (text2) => new Promise((resolve5, reject) => {
|
|
3059
|
+
process.stdout.write(text2, (error) => error ? reject(error) : resolve5());
|
|
3057
3060
|
});
|
|
3058
3061
|
const requestOptions = { signal: controller.signal };
|
|
3059
3062
|
const result = kind === "chat" ? await client.createChatCompletion(
|
|
@@ -5296,8 +5299,8 @@ async function withTimeout(promise, ms) {
|
|
|
5296
5299
|
try {
|
|
5297
5300
|
return await Promise.race([
|
|
5298
5301
|
promise,
|
|
5299
|
-
new Promise((
|
|
5300
|
-
timer = setTimeout(() =>
|
|
5302
|
+
new Promise((resolve5) => {
|
|
5303
|
+
timer = setTimeout(() => resolve5(TIMED_OUT), ms);
|
|
5301
5304
|
})
|
|
5302
5305
|
]);
|
|
5303
5306
|
} finally {
|
|
@@ -5567,12 +5570,12 @@ function hasBin(bin) {
|
|
|
5567
5570
|
}
|
|
5568
5571
|
}
|
|
5569
5572
|
function run(bin, args, label) {
|
|
5570
|
-
return new Promise((
|
|
5573
|
+
return new Promise((resolve5, reject) => {
|
|
5571
5574
|
const child = spawn(bin, args, { stdio: "inherit" });
|
|
5572
5575
|
child.on("error", (err) => reject(new Error(`Failed to start ${bin}: ${err.message}`)));
|
|
5573
5576
|
child.on("close", (code) => {
|
|
5574
5577
|
if (code !== 0) reject(new Error(`${label} failed with exit code ${code}`));
|
|
5575
|
-
else
|
|
5578
|
+
else resolve5();
|
|
5576
5579
|
});
|
|
5577
5580
|
});
|
|
5578
5581
|
}
|
|
@@ -6150,6 +6153,76 @@ async function backupTargets(opts) {
|
|
|
6150
6153
|
return results;
|
|
6151
6154
|
}
|
|
6152
6155
|
|
|
6156
|
+
// src/cli/commands/redteam-dashboard.ts
|
|
6157
|
+
import { lstat } from "fs/promises";
|
|
6158
|
+
import { resolve } from "path";
|
|
6159
|
+
import { RedTeamClient } from "@cdot65/prisma-airs-sdk";
|
|
6160
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
6161
|
+
function registerRedTeamDashboardCommand(redteam) {
|
|
6162
|
+
const command = redteam.command("dashboard").description("Generate a Red Team environment dashboard (read-only)").option("--output <format>", "Deliverable format: html or markdown (default: html)").option(
|
|
6163
|
+
"--output-file <path>",
|
|
6164
|
+
"Destination (default: new timestamped file in CWD); - for stdout; never overwrite"
|
|
6165
|
+
).option("--title <text>", "Report title", "Red Team environment report").option("--max-pages <n>", "Page budget per source, 1\u2013100 (15 records/page)", "40").option("--strict", "Exit 1 after writing if any source is incomplete").addHelpText(
|
|
6166
|
+
"after",
|
|
6167
|
+
examples(
|
|
6168
|
+
"airs redteam dashboard --output-file ./airs-daily.html",
|
|
6169
|
+
"airs redteam dashboard --output markdown --output-file ./airs-daily.md",
|
|
6170
|
+
"airs redteam dashboard",
|
|
6171
|
+
"airs redteam dashboard --strict --max-pages 20 --output-file - > daily.html"
|
|
6172
|
+
)
|
|
6173
|
+
).action(async (opts) => {
|
|
6174
|
+
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
6175
|
+
if (format !== "html" && format !== "markdown")
|
|
6176
|
+
usageError("Report output must be html or markdown");
|
|
6177
|
+
const maxPages = Number(opts.maxPages);
|
|
6178
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages) || maxPages > 100)
|
|
6179
|
+
usageError("--max-pages must be an integer from 1 to 100");
|
|
6180
|
+
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
6181
|
+
usageError("--title must contain 1\u2013240 characters");
|
|
6182
|
+
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
6183
|
+
usageError("--output-file cannot be empty");
|
|
6184
|
+
const defaultName = `airs-redteam-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID2().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
6185
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve(opts.outputFile ?? defaultName);
|
|
6186
|
+
try {
|
|
6187
|
+
if (outputPath) {
|
|
6188
|
+
const existing = await lstat(outputPath).catch((error) => {
|
|
6189
|
+
if (error.code !== "ENOENT") throw error;
|
|
6190
|
+
return void 0;
|
|
6191
|
+
});
|
|
6192
|
+
if (existing) throw new Error("Output already exists");
|
|
6193
|
+
}
|
|
6194
|
+
const config = await loadConfig();
|
|
6195
|
+
const client = new RedTeamClient({
|
|
6196
|
+
...redTeamClientOptions(config),
|
|
6197
|
+
numRetries: 0
|
|
6198
|
+
});
|
|
6199
|
+
ui.status("Collecting Red Team statistics and current configuration (read-only)...");
|
|
6200
|
+
const report = await collectRedTeamEnvironmentReport(client, {
|
|
6201
|
+
title: opts.title,
|
|
6202
|
+
maxPages
|
|
6203
|
+
});
|
|
6204
|
+
const rendered = format === "html" ? renderRedTeamReportHtml(report) : renderRedTeamReportMarkdown(report);
|
|
6205
|
+
if (outputPath) {
|
|
6206
|
+
await writeReportFile(outputPath, rendered);
|
|
6207
|
+
ui.status(`Report \u2192 ${outputPath.replace(/[\p{Cc}\p{Cf}]/gu, "_")} (mode 0600)`);
|
|
6208
|
+
} else {
|
|
6209
|
+
await new Promise(
|
|
6210
|
+
(done, reject) => process.stdout.write(rendered, (error) => error ? reject(error) : done())
|
|
6211
|
+
);
|
|
6212
|
+
}
|
|
6213
|
+
const incomplete = report.sources.filter((source) => source.status !== "complete").length;
|
|
6214
|
+
if (incomplete)
|
|
6215
|
+
ui.status(`${incomplete} source(s) incomplete; review the evidence section.`);
|
|
6216
|
+
if (report.health === "unknown" || opts.strict && incomplete > 0) process.exitCode = 1;
|
|
6217
|
+
} catch {
|
|
6218
|
+
ui.error(
|
|
6219
|
+
"Report generation failed. Verify Management API configuration and a writable, new output path. Existing files are never overwritten."
|
|
6220
|
+
);
|
|
6221
|
+
process.exitCode = 1;
|
|
6222
|
+
}
|
|
6223
|
+
});
|
|
6224
|
+
}
|
|
6225
|
+
|
|
6153
6226
|
// src/cli/commands/restore.ts
|
|
6154
6227
|
var SERVER_DERIVED_FIELDS = [
|
|
6155
6228
|
"uuid",
|
|
@@ -6392,6 +6465,7 @@ function parseAdapterVariables(input) {
|
|
|
6392
6465
|
}
|
|
6393
6466
|
function registerRedteamCommand(program) {
|
|
6394
6467
|
const redteam = program.command("redteam").description("AI Red Team scan operations");
|
|
6468
|
+
registerRedTeamDashboardCommand(redteam);
|
|
6395
6469
|
redteam.command("abort <jobId>").description("Abort a running scan").action(async (jobId) => {
|
|
6396
6470
|
try {
|
|
6397
6471
|
renderRedteamHeader();
|
|
@@ -7385,7 +7459,7 @@ function registerRedteamCommand(program) {
|
|
|
7385
7459
|
}
|
|
7386
7460
|
|
|
7387
7461
|
// src/cli/commands/runtime.ts
|
|
7388
|
-
import { randomUUID as
|
|
7462
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
7389
7463
|
import * as fs5 from "fs";
|
|
7390
7464
|
import { readFile as readFile10 } from "fs/promises";
|
|
7391
7465
|
import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
|
|
@@ -7633,7 +7707,7 @@ function mergeProfilePolicy(existing, overrides) {
|
|
|
7633
7707
|
}
|
|
7634
7708
|
|
|
7635
7709
|
// src/cli/bulk-scan-lock.ts
|
|
7636
|
-
import { randomUUID as
|
|
7710
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
7637
7711
|
import * as fs3 from "fs/promises";
|
|
7638
7712
|
function errorCode(error) {
|
|
7639
7713
|
return error?.code;
|
|
@@ -7664,7 +7738,7 @@ function processIsAlive(pid) {
|
|
|
7664
7738
|
}
|
|
7665
7739
|
}
|
|
7666
7740
|
async function installLock(lockPath, record) {
|
|
7667
|
-
const candidate = `${lockPath}.candidate-${process.pid}-${
|
|
7741
|
+
const candidate = `${lockPath}.candidate-${process.pid}-${randomUUID3()}`;
|
|
7668
7742
|
try {
|
|
7669
7743
|
await fs3.writeFile(candidate, JSON.stringify(record), {
|
|
7670
7744
|
encoding: "utf-8",
|
|
@@ -7682,7 +7756,7 @@ async function acquireBulkScanLock(statePath) {
|
|
|
7682
7756
|
version: 1,
|
|
7683
7757
|
pid: process.pid,
|
|
7684
7758
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7685
|
-
token:
|
|
7759
|
+
token: randomUUID3()
|
|
7686
7760
|
};
|
|
7687
7761
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
7688
7762
|
try {
|
|
@@ -7718,7 +7792,7 @@ async function acquireBulkScanLock(statePath) {
|
|
|
7718
7792
|
}
|
|
7719
7793
|
|
|
7720
7794
|
// src/cli/bulk-scan-state.ts
|
|
7721
|
-
import { randomUUID as
|
|
7795
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
7722
7796
|
import * as fs4 from "fs/promises";
|
|
7723
7797
|
import * as path2 from "path";
|
|
7724
7798
|
import { z } from "zod";
|
|
@@ -7815,8 +7889,8 @@ async function saveBulkScanState(state, dir, filePath) {
|
|
|
7815
7889
|
}
|
|
7816
7890
|
await fs4.mkdir(dir, { recursive: true, mode: 448 });
|
|
7817
7891
|
if (!filePath) await fs4.chmod(dir, 448);
|
|
7818
|
-
const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${
|
|
7819
|
-
const temporary = `${target}.tmp-${process.pid}-${
|
|
7892
|
+
const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${randomUUID4()}.bulk-scan.json`);
|
|
7893
|
+
const temporary = `${target}.tmp-${process.pid}-${randomUUID4()}`;
|
|
7820
7894
|
try {
|
|
7821
7895
|
await fs4.writeFile(temporary, JSON.stringify(state, null, 2), {
|
|
7822
7896
|
encoding: "utf-8",
|
|
@@ -8941,8 +9015,8 @@ function registerCleanupCommand(parent) {
|
|
|
8941
9015
|
}
|
|
8942
9016
|
|
|
8943
9017
|
// src/cli/commands/runtime-dashboard.ts
|
|
8944
|
-
import { lstat } from "fs/promises";
|
|
8945
|
-
import { resolve as
|
|
9018
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
9019
|
+
import { resolve as resolve3 } from "path";
|
|
8946
9020
|
import { dump as dump5 } from "js-yaml";
|
|
8947
9021
|
var TIME_UNITS = ["hour", "hours", "day", "days"];
|
|
8948
9022
|
function integer2(value, name, minimum = 0) {
|
|
@@ -9018,8 +9092,8 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9018
9092
|
"Scan content requires exactly one of --show-content or --output-file <new-path>"
|
|
9019
9093
|
);
|
|
9020
9094
|
if (opts.outputFile) {
|
|
9021
|
-
const path3 =
|
|
9022
|
-
const existing = await
|
|
9095
|
+
const path3 = resolve3(text(opts.outputFile, "--output-file"));
|
|
9096
|
+
const existing = await lstat2(path3).catch((error) => {
|
|
9023
9097
|
if (error.code !== "ENOENT") throw error;
|
|
9024
9098
|
return void 0;
|
|
9025
9099
|
});
|
|
@@ -9034,7 +9108,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9034
9108
|
const result = await read(client.dashboard);
|
|
9035
9109
|
const output = format === "yaml" ? dump5(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
|
|
9036
9110
|
if (command.name() === "scan-content" && opts.outputFile) {
|
|
9037
|
-
await writeReportFile(
|
|
9111
|
+
await writeReportFile(resolve3(String(opts.outputFile)), `${output}
|
|
9038
9112
|
`);
|
|
9039
9113
|
ui.status("Scan content exported to the requested private file (0600).");
|
|
9040
9114
|
} else console.log(output);
|
|
@@ -9205,9 +9279,9 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9205
9279
|
}
|
|
9206
9280
|
|
|
9207
9281
|
// src/cli/commands/runtime-report.ts
|
|
9208
|
-
import { lstat as
|
|
9209
|
-
import { resolve as
|
|
9210
|
-
import { randomUUID as
|
|
9282
|
+
import { lstat as lstat3 } from "fs/promises";
|
|
9283
|
+
import { resolve as resolve4 } from "path";
|
|
9284
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
9211
9285
|
function registerRuntimeReportCommand(runtime) {
|
|
9212
9286
|
const command = runtime.command("report").description("Generate a daily AI Runtime Security environment dashboard (read-only)").option("--output <format>", "Deliverable format: html or markdown (default: html)").option(
|
|
9213
9287
|
"--output-file <path>",
|
|
@@ -9231,11 +9305,11 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
9231
9305
|
usageError("--title must contain 1\u2013240 characters");
|
|
9232
9306
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
9233
9307
|
usageError("--output-file cannot be empty");
|
|
9234
|
-
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
9235
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
9308
|
+
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID5().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
9309
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve4(opts.outputFile ?? defaultName);
|
|
9236
9310
|
try {
|
|
9237
9311
|
if (outputPath) {
|
|
9238
|
-
const existing = await
|
|
9312
|
+
const existing = await lstat3(outputPath).catch((error) => {
|
|
9239
9313
|
if (error.code !== "ENOENT") throw error;
|
|
9240
9314
|
return void 0;
|
|
9241
9315
|
});
|
|
@@ -9722,7 +9796,7 @@ function completedBulkResults(state) {
|
|
|
9722
9796
|
}
|
|
9723
9797
|
async function writeBulkResults(outputPath, results) {
|
|
9724
9798
|
await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
|
|
9725
|
-
const temporary = `${outputPath}.tmp-${process.pid}-${
|
|
9799
|
+
const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID6()}`;
|
|
9726
9800
|
try {
|
|
9727
9801
|
await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
|
|
9728
9802
|
encoding: "utf-8",
|
|
@@ -10567,12 +10641,12 @@ function buildProgram() {
|
|
|
10567
10641
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
10568
10642
|
const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
|
|
10569
10643
|
setQuiet(Boolean(root.quiet));
|
|
10570
|
-
const
|
|
10571
|
-
if (
|
|
10644
|
+
const isEnvironmentReport = actionCommand.name() === "report" && actionCommand.parent?.name() === "runtime" || actionCommand.name() === "dashboard" && actionCommand.parent?.name() === "redteam";
|
|
10645
|
+
if (isEnvironmentReport && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
|
|
10572
10646
|
usageError(
|
|
10573
10647
|
"Disable --debug and PANW_AI_SEC_DEBUG for environment reports to avoid persisting sensitive traffic content"
|
|
10574
10648
|
);
|
|
10575
|
-
if (!
|
|
10649
|
+
if (!isEnvironmentReport && READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
|
|
10576
10650
|
try {
|
|
10577
10651
|
const format = await resolveOutput(actionCommand, actionCommand.opts());
|
|
10578
10652
|
actionCommand.setOptionValueWithSource("output", format, "implied");
|
|
@@ -10583,7 +10657,7 @@ function buildProgram() {
|
|
|
10583
10657
|
if (root.debug) {
|
|
10584
10658
|
const logPath = join4(
|
|
10585
10659
|
process.cwd(),
|
|
10586
|
-
`debug-api-${Date.now()}-${
|
|
10660
|
+
`debug-api-${Date.now()}-${randomUUID7().slice(0, 8)}.jsonl`
|
|
10587
10661
|
);
|
|
10588
10662
|
try {
|
|
10589
10663
|
installDebugLogger(logPath);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CreateCustomTopicRequest, CustomTopic as CustomTopic$1, CreateSecurityProfileRequest, ManagementClientOptions, ModelSecurityClientOptions, RedTeamClientOptions, InitOptions, ManagementClient } from '@cdot65/prisma-airs-sdk';
|
|
1
|
+
import { CreateCustomTopicRequest, CustomTopic as CustomTopic$1, CreateSecurityProfileRequest, ManagementClientOptions, ModelSecurityClientOptions, RedTeamClientOptions, InitOptions, ManagementClient, RedTeamClient, QuotaSummary } from '@cdot65/prisma-airs-sdk';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -1970,6 +1970,45 @@ interface RuntimeReportOptions {
|
|
|
1970
1970
|
now?: () => Date;
|
|
1971
1971
|
}
|
|
1972
1972
|
|
|
1973
|
+
/** Read-only SDK surface; older SDK installations explicitly report quota as unavailable. */
|
|
1974
|
+
type RedTeamReportClient = Pick<RedTeamClient, 'getDashboardOverview' | 'getScanStatistics'> & {
|
|
1975
|
+
targets: Pick<RedTeamClient['targets'], 'list'>;
|
|
1976
|
+
scans: Pick<RedTeamClient['scans'], 'list'>;
|
|
1977
|
+
adapters: Pick<RedTeamClient['adapters'], 'list'>;
|
|
1978
|
+
networkBroker: Pick<RedTeamClient['networkBroker'], 'getChannelStats'>;
|
|
1979
|
+
getQuotaSummary?: () => Promise<QuotaSummary>;
|
|
1980
|
+
};
|
|
1981
|
+
interface RedTeamReportTable {
|
|
1982
|
+
title: string;
|
|
1983
|
+
note: string;
|
|
1984
|
+
headers: string[];
|
|
1985
|
+
rows: Array<Array<string | number | null>>;
|
|
1986
|
+
}
|
|
1987
|
+
/** Aggregate-only projection: no target credentials, identities, scripts, prompts or raw responses. */
|
|
1988
|
+
interface RedTeamEnvironmentReport {
|
|
1989
|
+
schemaVersion: 1;
|
|
1990
|
+
product: 'Prisma AIRS AI Red Teaming';
|
|
1991
|
+
title: string;
|
|
1992
|
+
collectionStartedAt: string;
|
|
1993
|
+
generatedAt: string;
|
|
1994
|
+
window: {
|
|
1995
|
+
start: string;
|
|
1996
|
+
end: string;
|
|
1997
|
+
};
|
|
1998
|
+
health: 'attention' | 'review' | 'no-findings' | 'unknown';
|
|
1999
|
+
sources: ReportSource[];
|
|
2000
|
+
findings: ReportFinding[];
|
|
2001
|
+
tables: RedTeamReportTable[];
|
|
2002
|
+
limitations: string[];
|
|
2003
|
+
}
|
|
2004
|
+
/** Collect seven read-only feeds, with explicit source budgets and independent metric windows. */
|
|
2005
|
+
declare function collectRedTeamEnvironmentReport(client: RedTeamReportClient, options?: RuntimeReportOptions): Promise<RedTeamEnvironmentReport>;
|
|
2006
|
+
|
|
2007
|
+
/** Portable Markdown using the same allowlisted evidence as the HTML dashboard. */
|
|
2008
|
+
declare function renderRedTeamReportMarkdown(r: RedTeamEnvironmentReport): string;
|
|
2009
|
+
/** Self-contained HTML with hash-based CSP, static inline JS/CSS and no remote resources. */
|
|
2010
|
+
declare function renderRedTeamReportHtml(r: RedTeamEnvironmentReport): string;
|
|
2011
|
+
|
|
1973
2012
|
/** Render the same evidence model as a portable, inert Markdown deliverable. */
|
|
1974
2013
|
declare function renderRuntimeReportMarkdown(report: RuntimeDailyReport): string;
|
|
1975
2014
|
/** Standalone HTML: inline CSS/JS, no dependencies, external requests, or embedded raw data. */
|
|
@@ -1981,4 +2020,4 @@ declare function renderRuntimeReportHtml(report: RuntimeDailyReport): string;
|
|
|
1981
2020
|
*/
|
|
1982
2021
|
declare function collectRuntimeDailyReport(client: RuntimeReportClient, options?: RuntimeReportOptions): Promise<RuntimeDailyReport>;
|
|
1983
2022
|
|
|
1984
|
-
export { AirsScanService, type AnalysisReport, type ApiKeyInfo, type ApiKeyListResult, type BackupEnvelope, type BackupFormat, type BackupResult, type BatchEntry, type BulkScanAction, type BulkScanResult, type CategoryBreakdown, type ConsumptionAppListEntry, type ConsumptionQueryOptions, type ConsumptionTimeInterval, type CustomTopic, type CustomerAppConsumption, type CustomerAppInfo, type CustomerAppListResult, type DeleteResponse, type DeploymentProfileInfo, type EfficacyMetrics, type EulaContent, type EulaStatus, type IndexedPrompt, type InstanceDetail, type InstanceRequest, type InstanceResponse, type IterationResult, type ManagementService, type ModelSecurityEvaluation, type ModelSecurityFile, type ModelSecurityFileListOptions, type ModelSecurityGroup, type ModelSecurityGroupCreateRequest, type ModelSecurityGroupListOptions, type ModelSecurityGroupUpdateRequest, type ModelSecurityLabel, type ModelSecurityModel, type ModelSecurityModelListOptions, type ModelSecurityModelVersion, type ModelSecurityModelVersionListOptions, type ModelSecurityPyPIAuth, type ModelSecurityRule, type ModelSecurityRuleEditableField, type ModelSecurityRuleInstance, type ModelSecurityRuleInstanceListOptions, type ModelSecurityRuleInstanceUpdateRequest, type ModelSecurityRuleListOptions, type ModelSecurityScan, type ModelSecurityScanListOptions, type ModelSecurityService, type ModelSecurityViolation, type MutationResponse, type PaginationOptions, type PollRetryOptions, type ProfileTopic, type PromptDetail, type PromptSetDetail, type PromptSetService, type PromptSetVersionInfo, type PropertyValueList, type RedTeamAdapterCreateRequest, type RedTeamAdapterDetail, type RedTeamAdapterListItem, type RedTeamAdapterListOptions, type RedTeamAdapterUpdateOverrides, type RedTeamAdapterValidateRequest, type RedTeamAdapterValidationResult, type RedTeamAdapterVar, type RedTeamAttack, type RedTeamCategory, type RedTeamChannel, type RedTeamChannelCreateRequest, type RedTeamChannelListOptions, type RedTeamChannelStats, type RedTeamChannelUpdateRequest, type RedTeamCustomAttack, type RedTeamCustomReport, type RedTeamDynamicReport, type RedTeamErrorLog, type RedTeamJob, type RedTeamLanguages, type RedTeamService, type RedTeamStaticReport, type RedTeamTarget, type RedTeamTargetCreateRequest, type RedTeamTargetDetail, type RedTeamTargetUpdateRequest, type RegistryCredentials, type ReliableRuntimeService, type ReportApplication, type ReportDailyTelemetry, type ReportFinding, type ReportFormat, type ReportPriority, type ReportProfile, type ReportRegisteredApp, type ReportSessionSummary, type ReportSeverity, type ReportSource, type ReportSourceStatus, type ResourceType, type RestoreResult, type RunState, type RuntimeDailyReport, type RuntimeReportClient, type RuntimeReportOptions, type RuntimeScanResult, type RuntimeService, SDK_ASYNC_BATCH_SIZE, type ScanLogQueryOptions, type ScanLogQueryResult, type ScanResult, type ScanService, SdkManagementService, SdkModelSecurityService, SdkPromptSetService, SdkRedTeamService, SdkRuntimeService, type SecurityProfileInfo, type SecurityProfileListResult, type SubmittedBatch, type TargetAuthValidationRequest, type TargetAuthValidationResult, type TargetOperationOptions, type TestCase, type TestResult, type UserInput, type ValidationError, collectRuntimeDailyReport, computeCategoryBreakdown, computeMetrics, loadConfig, readBackupDir, readBackupFile, renderRuntimeReportHtml, renderRuntimeReportMarkdown, resolveOutputDir, sanitizeFilename, validateDescription, validateExamples, validateName, validateTopic, writeBackupFile, writeReportFile };
|
|
2023
|
+
export { AirsScanService, type AnalysisReport, type ApiKeyInfo, type ApiKeyListResult, type BackupEnvelope, type BackupFormat, type BackupResult, type BatchEntry, type BulkScanAction, type BulkScanResult, type CategoryBreakdown, type ConsumptionAppListEntry, type ConsumptionQueryOptions, type ConsumptionTimeInterval, type CustomTopic, type CustomerAppConsumption, type CustomerAppInfo, type CustomerAppListResult, type DeleteResponse, type DeploymentProfileInfo, type EfficacyMetrics, type EulaContent, type EulaStatus, type IndexedPrompt, type InstanceDetail, type InstanceRequest, type InstanceResponse, type IterationResult, type ManagementService, type ModelSecurityEvaluation, type ModelSecurityFile, type ModelSecurityFileListOptions, type ModelSecurityGroup, type ModelSecurityGroupCreateRequest, type ModelSecurityGroupListOptions, type ModelSecurityGroupUpdateRequest, type ModelSecurityLabel, type ModelSecurityModel, type ModelSecurityModelListOptions, type ModelSecurityModelVersion, type ModelSecurityModelVersionListOptions, type ModelSecurityPyPIAuth, type ModelSecurityRule, type ModelSecurityRuleEditableField, type ModelSecurityRuleInstance, type ModelSecurityRuleInstanceListOptions, type ModelSecurityRuleInstanceUpdateRequest, type ModelSecurityRuleListOptions, type ModelSecurityScan, type ModelSecurityScanListOptions, type ModelSecurityService, type ModelSecurityViolation, type MutationResponse, type PaginationOptions, type PollRetryOptions, type ProfileTopic, type PromptDetail, type PromptSetDetail, type PromptSetService, type PromptSetVersionInfo, type PropertyValueList, type RedTeamAdapterCreateRequest, type RedTeamAdapterDetail, type RedTeamAdapterListItem, type RedTeamAdapterListOptions, type RedTeamAdapterUpdateOverrides, type RedTeamAdapterValidateRequest, type RedTeamAdapterValidationResult, type RedTeamAdapterVar, type RedTeamAttack, type RedTeamCategory, type RedTeamChannel, type RedTeamChannelCreateRequest, type RedTeamChannelListOptions, type RedTeamChannelStats, type RedTeamChannelUpdateRequest, type RedTeamCustomAttack, type RedTeamCustomReport, type RedTeamDynamicReport, type RedTeamEnvironmentReport, type RedTeamErrorLog, type RedTeamJob, type RedTeamLanguages, type RedTeamReportClient, type RedTeamReportTable, type RedTeamService, type RedTeamStaticReport, type RedTeamTarget, type RedTeamTargetCreateRequest, type RedTeamTargetDetail, type RedTeamTargetUpdateRequest, type RegistryCredentials, type ReliableRuntimeService, type ReportApplication, type ReportDailyTelemetry, type ReportFinding, type ReportFormat, type ReportPriority, type ReportProfile, type ReportRegisteredApp, type ReportSessionSummary, type ReportSeverity, type ReportSource, type ReportSourceStatus, type ResourceType, type RestoreResult, type RunState, type RuntimeDailyReport, type RuntimeReportClient, type RuntimeReportOptions, type RuntimeScanResult, type RuntimeService, SDK_ASYNC_BATCH_SIZE, type ScanLogQueryOptions, type ScanLogQueryResult, type ScanResult, type ScanService, SdkManagementService, SdkModelSecurityService, SdkPromptSetService, SdkRedTeamService, SdkRuntimeService, type SecurityProfileInfo, type SecurityProfileListResult, type SubmittedBatch, type TargetAuthValidationRequest, type TargetAuthValidationResult, type TargetOperationOptions, type TestCase, type TestResult, type UserInput, type ValidationError, collectRedTeamEnvironmentReport, collectRuntimeDailyReport, computeCategoryBreakdown, computeMetrics, loadConfig, readBackupDir, readBackupFile, renderRedTeamReportHtml, renderRedTeamReportMarkdown, renderRuntimeReportHtml, renderRuntimeReportMarkdown, resolveOutputDir, sanitizeFilename, validateDescription, validateExamples, validateName, validateTopic, writeBackupFile, writeReportFile };
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,15 @@ import {
|
|
|
6
6
|
SdkPromptSetService,
|
|
7
7
|
SdkRedTeamService,
|
|
8
8
|
SdkRuntimeService,
|
|
9
|
+
collectRedTeamEnvironmentReport,
|
|
9
10
|
collectRuntimeDailyReport,
|
|
10
11
|
computeCategoryBreakdown,
|
|
11
12
|
computeMetrics,
|
|
12
13
|
loadConfig,
|
|
13
14
|
readBackupDir,
|
|
14
15
|
readBackupFile,
|
|
16
|
+
renderRedTeamReportHtml,
|
|
17
|
+
renderRedTeamReportMarkdown,
|
|
15
18
|
renderRuntimeReportHtml,
|
|
16
19
|
renderRuntimeReportMarkdown,
|
|
17
20
|
resolveOutputDir,
|
|
@@ -22,7 +25,7 @@ import {
|
|
|
22
25
|
validateTopic,
|
|
23
26
|
writeBackupFile,
|
|
24
27
|
writeReportFile
|
|
25
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-DR6MZG4E.js";
|
|
26
29
|
export {
|
|
27
30
|
AirsScanService,
|
|
28
31
|
SDK_ASYNC_BATCH_SIZE,
|
|
@@ -31,12 +34,15 @@ export {
|
|
|
31
34
|
SdkPromptSetService,
|
|
32
35
|
SdkRedTeamService,
|
|
33
36
|
SdkRuntimeService,
|
|
37
|
+
collectRedTeamEnvironmentReport,
|
|
34
38
|
collectRuntimeDailyReport,
|
|
35
39
|
computeCategoryBreakdown,
|
|
36
40
|
computeMetrics,
|
|
37
41
|
loadConfig,
|
|
38
42
|
readBackupDir,
|
|
39
43
|
readBackupFile,
|
|
44
|
+
renderRedTeamReportHtml,
|
|
45
|
+
renderRedTeamReportMarkdown,
|
|
40
46
|
renderRuntimeReportHtml,
|
|
41
47
|
renderRuntimeReportMarkdown,
|
|
42
48
|
resolveOutputDir,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdot65/prisma-airs-cli",
|
|
3
3
|
"packageManager": "pnpm@10.6.5",
|
|
4
|
-
"version": "5.0
|
|
4
|
+
"version": "5.1.0",
|
|
5
5
|
"description": "CLI and library for Palo Alto Prisma AIRS — guardrail refinement, AI red teaming, model security scanning, profile audits",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"license": "MIT",
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@cdot65/prisma-airs-sdk": "0.
|
|
62
|
+
"@cdot65/prisma-airs-sdk": "0.27.0",
|
|
63
63
|
"@inquirer/prompts": "^8.3.0",
|
|
64
64
|
"chalk": "^5.6.2",
|
|
65
65
|
"commander": "^14.0.3",
|