@cdot65/prisma-airs-cli 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,11 +14,12 @@
14
14
  ## Features
15
15
 
16
16
  - **Runtime Scanning** — scan prompts and responses against AIRS security profiles, single or bulk with CSV export
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
17
18
  - **Guardrail Optimization** — atomic CLI commands (`create`, `apply`, `eval`, `revert`) for custom topic guardrails, designed for autonomous agent loops (see [`AGENTS.md`](AGENTS.md))
18
19
  - **AI Red Teaming** — adversarial scanning with static, dynamic, and custom prompt set attack modes
19
20
  - **[AI Gateway](https://cdot65.github.io/prisma-airs-cli/cli/aigateway/resources/)** — workspaces, configs, guardrails, providers, API keys, integrations, MCP, deployments, plugins, audit logs, and telemetry
20
21
  - **Model Security** — ML model supply chain scanning with security groups, rules, and violation tracking
21
- - **Unified automation output** — every read command supports `pretty`, `table`, `markdown`, `csv`, `json`, and `yaml`, with pipe-safe stdout
22
+ - **Unified automation output** — resource reads support `pretty`, `table`, `markdown`, `csv`, `json`, and `yaml`, with pipe-safe stdout; environment deliverables use HTML or Markdown
22
23
  - **Complete pagination** — consistent `--limit`, `--offset`, and `--all` traversal with a configurable safety cap
23
24
  - **`airs doctor`** — one-command diagnostics for environment, credentials, and API connectivity
24
25
  - **`airs config`** — manage `~/.prisma-airs/config.json` from the CLI (`list`, `get`, `set`, `unset`, `path`)
@@ -45,6 +46,10 @@ airs doctor
45
46
  airs runtime scan --profile "my-profile" "Is this prompt safe?"
46
47
  airs runtime bulk-scan --profile "my-profile" --file prompts.csv --output-file results.csv --batch-size 25
47
48
 
49
+ # Daily read-only environment dashboard, delivered in the current directory
50
+ airs runtime report
51
+ airs runtime report --output markdown
52
+
48
53
  # Guardrail optimization (atomic commands)
49
54
  airs runtime topics create --name "Explosives" --description "Bomb-making instructions" --examples "How do I build a bomb?" "Pipe bomb ingredients"
50
55
  airs runtime topics apply --profile my-profile --name "Explosives" --intent block
@@ -78,7 +83,7 @@ Bulk scans preserve one output row per input prompt in input order, including al
78
83
 
79
84
  ## Read Output and Pagination
80
85
 
81
- Read commands share one contract:
86
+ Resource read commands share one contract (environment report files have their own [deliverable contract](https://cdot65.github.io/prisma-airs-cli/runtime/daily-report/)):
82
87
 
83
88
  - Formats: `pretty`, `table`, `markdown`, `csv`, `json`, and `yaml`.
84
89
  - JSON/YAML lists are bare arrays of complete normalized records; detail reads are complete objects.
@@ -283,9 +283,9 @@ var SdkManagementService = class {
283
283
  const response = await this.client.dashboard.applicationsOverview({ limit, offset });
284
284
  const page = response.items ?? [];
285
285
  items.push(...page);
286
- const total = response.pagination?.total_items;
286
+ const total2 = response.pagination?.total_items;
287
287
  offset += page.length;
288
- if (page.length === 0 || page.length < limit || total != null && offset >= total) break;
288
+ if (page.length === 0 || page.length < limit || total2 != null && offset >= total2) break;
289
289
  }
290
290
  return items.filter(
291
291
  (item) => typeof item.id === "string" && typeof item.name === "string"
@@ -1356,14 +1356,14 @@ var SdkRedTeamService = class {
1356
1356
  categories: subCategories.map((sc) => {
1357
1357
  const successful = sc.successful ?? 0;
1358
1358
  const failed = sc.failed ?? 0;
1359
- const total = sc.total ?? successful + failed;
1359
+ const total2 = sc.total ?? successful + failed;
1360
1360
  return {
1361
1361
  id: sc.id,
1362
1362
  displayName: sc.display_name,
1363
- asr: total > 0 ? successful / total * 100 : 0,
1363
+ asr: total2 > 0 ? successful / total2 * 100 : 0,
1364
1364
  successful,
1365
1365
  failed,
1366
- total
1366
+ total: total2
1367
1367
  };
1368
1368
  })
1369
1369
  };
@@ -1683,11 +1683,11 @@ function isDefiniteRateLimitError(err) {
1683
1683
  return metadata?.failureKind === "http" && metadata.statusCode === 429;
1684
1684
  }
1685
1685
  function runtimeDetections(value) {
1686
- const source = value ?? {};
1686
+ const source2 = value ?? {};
1687
1687
  return Object.fromEntries(
1688
- RUNTIME_DETECTION_KEYS.filter((key) => typeof source[key] === "boolean").map((key) => [
1688
+ RUNTIME_DETECTION_KEYS.filter((key) => typeof source2[key] === "boolean").map((key) => [
1689
1689
  key,
1690
- source[key]
1690
+ source2[key]
1691
1691
  ])
1692
1692
  );
1693
1693
  }
@@ -2342,8 +2342,8 @@ async function inspectConfig(configFilePath) {
2342
2342
  const config = ConfigSchema.parse(merged);
2343
2343
  const entries = {};
2344
2344
  for (const key of Object.keys(ConfigSchema.shape)) {
2345
- const source = key in envConfig ? "env" : key in fileConfig ? "file" : "default";
2346
- entries[key] = { value: config[key], source };
2345
+ const source2 = key in envConfig ? "env" : key in fileConfig ? "file" : "default";
2346
+ entries[key] = { value: config[key], source: source2 };
2347
2347
  }
2348
2348
  return entries;
2349
2349
  }
@@ -2441,10 +2441,10 @@ function computeMetrics(results) {
2441
2441
  }
2442
2442
  const totalPositives = truePositives + falseNegatives;
2443
2443
  const totalNegatives = trueNegatives + falsePositives;
2444
- const total = results.length;
2444
+ const total2 = results.length;
2445
2445
  const truePositiveRate = totalPositives > 0 ? truePositives / totalPositives : 0;
2446
2446
  const trueNegativeRate = totalNegatives > 0 ? trueNegatives / totalNegatives : 0;
2447
- const accuracy = total > 0 ? (truePositives + trueNegatives) / total : 0;
2447
+ const accuracy = total2 > 0 ? (truePositives + trueNegatives) / total2 : 0;
2448
2448
  const coverage = Math.min(truePositiveRate, trueNegativeRate);
2449
2449
  const precision = truePositives + falsePositives > 0 ? truePositives / (truePositives + falsePositives) : 0;
2450
2450
  const recall = truePositiveRate;
@@ -2472,15 +2472,646 @@ function computeCategoryBreakdown(results) {
2472
2472
  if (r.testCase.expectedTriggered && !r.actualTriggered) entry.fn++;
2473
2473
  map.set(cat, entry);
2474
2474
  }
2475
- return [...map.entries()].map(([category, { total, fp, fn }]) => ({
2475
+ return [...map.entries()].map(([category, { total: total2, fp, fn }]) => ({
2476
2476
  category,
2477
- total,
2477
+ total: total2,
2478
2478
  fp,
2479
2479
  fn,
2480
- errorRate: total > 0 ? (fp + fn) / total : 0
2480
+ errorRate: total2 > 0 ? (fp + fn) / total2 : 0
2481
2481
  })).sort((a, b) => b.errorRate - a.errorRate);
2482
2482
  }
2483
2483
 
2484
+ // src/reports/io.ts
2485
+ import { randomUUID } from "crypto";
2486
+ import { link, open, unlink } from "fs/promises";
2487
+ import { dirname, join as join2 } from "path";
2488
+ async function writeReportFile(path2, content) {
2489
+ const temporary = join2(dirname(path2), `.airs-report-${randomUUID()}.tmp`);
2490
+ const file = await open(temporary, "wx", 384);
2491
+ try {
2492
+ await file.writeFile(content, "utf8");
2493
+ await file.sync();
2494
+ await file.close();
2495
+ await link(temporary, path2);
2496
+ } finally {
2497
+ await file.close();
2498
+ await unlink(temporary);
2499
+ }
2500
+ }
2501
+
2502
+ // src/reports/render.ts
2503
+ import { createHash } from "crypto";
2504
+ var HEALTH = {
2505
+ attention: "Attention required",
2506
+ review: "Review required",
2507
+ "no-findings": "No findings in available evidence",
2508
+ unknown: "Unable to assess"
2509
+ };
2510
+ function html(value) {
2511
+ return String(value ?? "Unknown").replace(
2512
+ /[&<>"']/g,
2513
+ (char) => ({
2514
+ "&": "&amp;",
2515
+ "<": "&lt;",
2516
+ ">": "&gt;",
2517
+ '"': "&quot;",
2518
+ "'": "&#39;"
2519
+ })[char] ?? char
2520
+ );
2521
+ }
2522
+ function md(value) {
2523
+ return String(value ?? "Unknown").replace(
2524
+ /[&<>"'\\`*_[\]{}()|!#~\r\n]/g,
2525
+ (char) => char === "\r" || char === "\n" ? " " : `&#${char.charCodeAt(0)};`
2526
+ );
2527
+ }
2528
+ function number(value) {
2529
+ return value === null ? "Unknown" : value.toLocaleString("en-US", { maximumFractionDigits: 2 });
2530
+ }
2531
+ function tables(report) {
2532
+ const logSource = report.sources.find((source2) => source2.name === "Scan log detail");
2533
+ return [
2534
+ {
2535
+ title: "Daily application activity",
2536
+ id: "applications",
2537
+ headers: [
2538
+ "Application bucket",
2539
+ "Sessions",
2540
+ "Violating sessions",
2541
+ "Violation rate",
2542
+ "Registered ID match"
2543
+ ],
2544
+ rows: report.activity.applications.map((app) => [
2545
+ app.name,
2546
+ number(app.sessions),
2547
+ number(app.violatingSessions),
2548
+ app.sessions !== null && app.sessions > 0 && app.violatingSessions !== null && app.violatingSessions <= app.sessions ? `${number(app.violatingSessions / app.sessions * 100)}%` : "Unknown",
2549
+ app.registered === null ? "Unknown" : app.registered ? "Yes" : "Not in returned inventory"
2550
+ ]),
2551
+ note: "API-reported rolling one-day application buckets, ranked by violating sessions. Partial-source totals cover collected buckets only. Violating sessions are not necessarily blocked sessions."
2552
+ },
2553
+ {
2554
+ title: "Current security profiles",
2555
+ id: "profiles",
2556
+ headers: [
2557
+ "Profile",
2558
+ "Revision",
2559
+ "Active",
2560
+ "Timeout action",
2561
+ "Storage masking",
2562
+ "Last modified (UTC)"
2563
+ ],
2564
+ rows: report.profiles.map((profile) => [
2565
+ profile.name,
2566
+ number(profile.revision),
2567
+ profile.active === null ? "Unknown" : profile.active ? "Yes" : "No",
2568
+ profile.timeoutActions.join(", ") || "Unknown",
2569
+ profile.storageMasking,
2570
+ profile.modified
2571
+ ]),
2572
+ note: "Latest returned revision per profile name. These are current settings, not a record of changes during the daily window."
2573
+ },
2574
+ {
2575
+ title: "Registered application inventory",
2576
+ id: "inventory",
2577
+ headers: ["Application", "Environment", "Cloud", "Model", "Key associations"],
2578
+ rows: report.registeredApps.map((app) => [
2579
+ app.name,
2580
+ app.environment,
2581
+ app.cloud,
2582
+ app.model,
2583
+ number(app.keyAssociations)
2584
+ ]),
2585
+ note: "Current registered applications, separate from scan-metadata application buckets. Association counts do not establish key validity or deployment health. No key values or auth codes are included."
2586
+ },
2587
+ {
2588
+ title: "Collected scan-log observations",
2589
+ id: "logs",
2590
+ headers: ["Dimension", "API value", "Entries"],
2591
+ rows: [
2592
+ ...report.logs.actions.map((entry) => ["Action", entry.name, number(entry.count)]),
2593
+ ...report.logs.verdicts.map((entry) => ["Verdict", entry.name, number(entry.count)])
2594
+ ],
2595
+ note: `Source: ${logSource?.status ?? "unavailable"}. Timestamp-eligible entries: ${logSource?.status === "unavailable" ? "Unknown" : number(report.logs.entries)}. Tokens across eligible collected entries: ${number(report.logs.tokens)}. Missing timestamps: ${report.logs.missingTimestamps}; outside window: ${report.logs.outsideWindow}. These are sample/collection counts, not tenant totals.`
2596
+ },
2597
+ {
2598
+ title: "Evidence and collection coverage",
2599
+ id: "evidence",
2600
+ headers: ["Source / SDK method", "Window", "Status", "Records", "Pages", "Collection notes"],
2601
+ rows: report.sources.map((source2) => [
2602
+ `${source2.name} \u2014 ${source2.method}`,
2603
+ source2.window,
2604
+ source2.status,
2605
+ number(source2.records),
2606
+ source2.pages,
2607
+ source2.notes.join(" ") || "Collection completed."
2608
+ ]),
2609
+ note: "Complete means pagination finished for that source, not that the environment is secure or that all traffic was ingested. Unavailable does not mean zero."
2610
+ }
2611
+ ];
2612
+ }
2613
+ function metrics(report) {
2614
+ return [
2615
+ ["Sessions in collected app buckets", number(report.activity.sessions)],
2616
+ ["Violating sessions", number(report.activity.violatingSessions)],
2617
+ [
2618
+ "Violation rate",
2619
+ report.activity.violationRate === null ? "Unknown" : `${number(report.activity.violationRate)}%`
2620
+ ],
2621
+ [
2622
+ "Complete evidence sources",
2623
+ `${report.sources.filter((source2) => source2.status === "complete").length} / ${report.sources.length}`
2624
+ ]
2625
+ ];
2626
+ }
2627
+ function renderRuntimeReportMarkdown(report) {
2628
+ const lines = [
2629
+ `# ${md(report.title)}`,
2630
+ "",
2631
+ `**${md(report.product)}**`,
2632
+ "",
2633
+ `Assessment: **${HEALTH[report.health]}**`,
2634
+ "",
2635
+ `Window: ${md(report.window.start)} \u2192 ${md(report.window.end)}`,
2636
+ "",
2637
+ `${md(report.window.description)}. Collection: ${md(report.collectionStartedAt)} \u2192 ${md(report.generatedAt)}.`,
2638
+ "",
2639
+ "## At a glance",
2640
+ "",
2641
+ ...metrics(report).map(([key, value]) => `- ${key}: **${value}**`),
2642
+ "",
2643
+ "## What needs attention",
2644
+ ""
2645
+ ];
2646
+ if (report.findings.length === 0)
2647
+ lines.push(
2648
+ "No findings in the available evidence. This does not establish an absence of risk.",
2649
+ ""
2650
+ );
2651
+ for (const finding of report.findings)
2652
+ lines.push(
2653
+ `### ${finding.priority.toUpperCase()}: ${md(finding.title)}`,
2654
+ "",
2655
+ `Evidence: ${md(finding.evidence)}`,
2656
+ "",
2657
+ `Next step: ${md(finding.recommendation)}`,
2658
+ "",
2659
+ `Source: ${md(finding.source)}`,
2660
+ ""
2661
+ );
2662
+ for (const table of tables(report)) {
2663
+ lines.push(`## ${table.title}`, "", md(table.note), "");
2664
+ if (table.rows.length === 0)
2665
+ lines.push(
2666
+ "No rows available. Consult the source status before interpreting this as an empty result.",
2667
+ ""
2668
+ );
2669
+ else
2670
+ lines.push(
2671
+ `| ${table.headers.join(" | ")} |`,
2672
+ `| ${table.headers.map(() => "---").join(" | ")} |`,
2673
+ ...table.rows.map((row) => `| ${row.map(md).join(" | ")} |`),
2674
+ ""
2675
+ );
2676
+ }
2677
+ lines.push(
2678
+ "## Scope, privacy, and limitations",
2679
+ "",
2680
+ ...report.limitations.map((note) => `- ${md(note)}`),
2681
+ "",
2682
+ "Generated by Prisma AIRS CLI \xB7 Report schema 1",
2683
+ ""
2684
+ );
2685
+ return lines.join("\n");
2686
+ }
2687
+ var CSS = `
2688
+ #evidence td:nth-child(3){white-space:nowrap}
2689
+ :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}
2690
+ *{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}
2691
+ main{max-width:1280px;margin:auto;padding:1.5rem 1.25rem 3rem}nav{display:flex;gap:1.25rem;flex-wrap:wrap;margin-bottom:1.5rem}section{background:#fff;border:1px solid var(--line);border-radius:12px;padding:1.4rem;margin:1.3rem 0}.cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1rem}.card{background:#fff;border:1px solid var(--line);border-top:4px solid var(--accent);border-radius:9px;padding:1.2rem}.card strong{display:block;font-size:2rem;font-variant-numeric:tabular-nums}.card span,.note{color:var(--muted);font-size:.9rem}.toolbar{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;margin:1rem 0}button,input,select{font:inherit;border:1px solid #8b9eaf;border-radius:6px;padding:.5rem .7rem;background:#fff;color:var(--ink)}button{cursor:pointer}input{max-width:100%}:focus-visible{outline:3px solid #c06605;outline-offset:3px}.finding{border-left:4px solid #b3c3cf;padding:.5rem 1rem;margin:1rem 0;background:#f7f9fb}.finding[data-priority=attention]{border-color:#c74429}.finding[data-priority=review]{border-color:#b78712}.priority{font-size:.75rem;text-transform:uppercase;font-weight:750;letter-spacing:.07em}.finding p{margin:.4rem 0}.table-wrap{overflow:auto}table{border-collapse:collapse;width:100%;font-size:.9rem;text-align:left}th{background:#edf3f7;font-weight:700}th,td{padding:.7rem .8rem;border-bottom:1px solid var(--line);vertical-align:top;overflow-wrap:anywhere;min-width:90px}tbody tr:hover{background:#f8fbfd}caption{text-align:left;font-weight:600;padding:.5rem 0}footer{color:var(--muted);font-size:.8rem;margin-top:2rem}.hidden,[hidden]{display:none!important}.js-only{display:none}.js-ready .js-only{display:flex}.skip{position:absolute;left:-9999px}.skip:focus{left:1rem;top:1rem;background:#fff;padding:1rem;z-index:1}li{margin:.5rem 0}
2692
+ @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}}
2693
+ @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}}
2694
+ `;
2695
+ var JS = `
2696
+ 'use strict';
2697
+ document.documentElement.classList.add('js-ready');
2698
+ document.getElementById('print-report').addEventListener('click',function(){window.print();});
2699
+ var filter=document.getElementById('finding-filter');
2700
+ filter.addEventListener('change',function(){
2701
+ var visible=0;
2702
+ document.querySelectorAll('.finding').forEach(function(row){row.hidden=filter.value!=='all'&&row.dataset.priority!==filter.value;if(!row.hidden)visible++;});
2703
+ document.getElementById('finding-count').textContent=visible+' findings shown';
2704
+ });
2705
+ var search=document.getElementById('app-search');
2706
+ search.addEventListener('input',function(){
2707
+ var query=search.value.toLowerCase().trim(),visible=0;
2708
+ document.querySelectorAll('#applications tbody tr').forEach(function(row){row.hidden=!row.textContent.toLowerCase().includes(query);if(!row.hidden)visible++;});
2709
+ document.getElementById('app-count').textContent=visible+' application buckets shown';
2710
+ });
2711
+ `;
2712
+ function renderRuntimeReportHtml(report) {
2713
+ const hash = (text) => createHash("sha256").update(text).digest("base64");
2714
+ const policy = `default-src 'none'; script-src 'sha256-${hash(JS)}'; style-src 'sha256-${hash(CSS)}'; base-uri 'none'; form-action 'none'; connect-src 'none'; img-src 'none'`;
2715
+ const findingCards = report.findings.map(
2716
+ (finding) => `<article class="finding" data-priority="${html(finding.priority)}"><span class="priority">${html(finding.priority)}</span><h3>${html(finding.title)}</h3><p><strong>Evidence:</strong> ${html(finding.evidence)}</p><p><strong>Next step:</strong> ${html(finding.recommendation)}</p><p class="note">Source: ${html(finding.source)}</p></article>`
2717
+ ).join("");
2718
+ const sections = tables(report).map(
2719
+ (table) => `<section id="${table.id}" aria-labelledby="${table.id}-title"><h2 id="${table.id}-title">${table.title}</h2><p class="note">${html(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>${html(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>`
2720
+ ).join("");
2721
+ return `<!doctype html>
2722
+ <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(report.title)} \u2014 Prisma AIRS</title><style>${CSS}</style></head>
2723
+ <body class="${html(report.health)}"><a class="skip" href="#main">Skip to report</a><header><div class="eyebrow">Prisma AIRS \xB7 AI Runtime Security</div><h1>${html(report.title)}</h1><span class="badge">${HEALTH[report.health]}</span><p>${html(report.window.description)}<br><time>${html(report.window.start)}</time> \u2192 <time>${html(report.window.end)}</time></p><p>Collected ${html(report.collectionStartedAt)} \u2192 ${html(report.generatedAt)}<br>Read-only evidence \xB7 Confidential environment metadata</p></header>
2724
+ <main id="main"><nav aria-label="Report sections"><a href="#findings">Attention</a><a href="#applications">Activity</a><a href="#profiles">Profiles</a><a href="#inventory">Inventory</a><a href="#evidence">Evidence</a><a href="#scope">Scope</a></nav>
2725
+ <div class="cards">${metrics(report).map(([name, value]) => `<div class="card"><span>${name}</span><strong>${value}</strong></div>`).join("")}</div>
2726
+ <section id="findings"><h2>What needs attention</h2><p class="note">Observed violations and configuration review items, not a numerical health score. Collection gaps remain visible in the evidence table.</p><div class="toolbar js-only"><label for="finding-filter">Priority</label><select id="finding-filter"><option value="all">All findings</option><option value="attention">Attention</option><option value="review">Review</option><option value="info">Information</option></select><span id="finding-count" role="status"></span><button id="print-report" type="button">Print / save PDF</button></div>${findingCards || "<p>No findings in the available evidence. This does not establish an absence of risk.</p>"}</section>
2727
+ ${sections}<section id="scope"><h2>Scope, privacy, and limitations</h2><ul>${report.limitations.map((note) => `<li>${html(note)}</li>`).join("")}</ul></section><footer>Generated by Prisma AIRS CLI \xB7 Report schema 1 \xB7 Self-contained HTML; no network access required. All evidence remains readable with JavaScript disabled.</footer></main><script>${JS}</script></body></html>
2728
+ `;
2729
+ }
2730
+
2731
+ // src/reports/runtime.ts
2732
+ var PAGE_SIZE = 100;
2733
+ var DAY_MS = 864e5;
2734
+ var ReportDataError = class extends Error {
2735
+ };
2736
+ function safeFailure(error) {
2737
+ if (error instanceof ReportDataError) return error.message;
2738
+ const code = error?.statusCode;
2739
+ if (typeof code === "number" && Number.isInteger(code) && code >= 400 && code <= 599) {
2740
+ if (code === 401 || code === 403)
2741
+ return `HTTP ${code}: check Management API credentials and grants.`;
2742
+ if (code === 429) return "HTTP 429: rate limited; retry the report later.";
2743
+ return `HTTP ${code}: source request failed; verify service availability and API compatibility.`;
2744
+ }
2745
+ return "Source request or response validation failed; check connectivity and SDK compatibility.";
2746
+ }
2747
+ function label(value) {
2748
+ return (value?.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim() || "Unknown").slice(0, 240);
2749
+ }
2750
+ function count(value) {
2751
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
2752
+ }
2753
+ async function collect(source2, maxPages, read, identity) {
2754
+ const records = /* @__PURE__ */ new Map();
2755
+ let offset = 0;
2756
+ try {
2757
+ for (let page = 0; page < maxPages; page++) {
2758
+ const result = await read(offset);
2759
+ source2.pages++;
2760
+ if (!Array.isArray(result.items))
2761
+ throw new ReportDataError(
2762
+ "The API returned no record array. An empty object or absent collection does not establish zero activity."
2763
+ );
2764
+ const before = records.size;
2765
+ for (const item of result.items) records.set(identity(item), item);
2766
+ source2.records = records.size;
2767
+ if (result.note) source2.notes.push(result.note);
2768
+ if (records.size - before !== result.items.length) {
2769
+ source2.notes.push("Overlapping or repeated records detected; duplicates excluded.");
2770
+ source2.status = "partial";
2771
+ }
2772
+ if (result.next === void 0) {
2773
+ if (source2.status !== "partial") source2.status = "complete";
2774
+ return [...records.values()];
2775
+ }
2776
+ if (!Number.isSafeInteger(result.next) || result.next <= offset || records.size === before) {
2777
+ source2.notes.push("Pagination did not advance; collection stopped safely.");
2778
+ source2.status = "partial";
2779
+ return [...records.values()];
2780
+ }
2781
+ offset = result.next;
2782
+ }
2783
+ source2.notes.push(`Reached the ${maxPages}-page budget; this is not a complete inventory.`);
2784
+ source2.status = "partial";
2785
+ } catch (error) {
2786
+ source2.status = source2.pages > 0 && records.size > 0 ? "partial" : "unavailable";
2787
+ source2.notes.push(safeFailure(error));
2788
+ }
2789
+ return [...records.values()];
2790
+ }
2791
+ function source(name, method, window) {
2792
+ return { name, method, window, status: "unavailable", records: 0, pages: 0, notes: [] };
2793
+ }
2794
+ function inventoryNext(next, size, offset) {
2795
+ return next === 0 ? void 0 : next ?? (size === PAGE_SIZE ? offset + size : void 0);
2796
+ }
2797
+ function total(values) {
2798
+ if (values.some((value) => value === null)) return null;
2799
+ const result = values.reduce((sum, value) => sum + (value ?? 0), 0);
2800
+ return count(result);
2801
+ }
2802
+ function distribution(rows, key) {
2803
+ const groups = /* @__PURE__ */ new Map();
2804
+ for (const row of rows) {
2805
+ const name = label(row[key]).toLowerCase();
2806
+ groups.set(name, (groups.get(name) ?? 0) + 1);
2807
+ }
2808
+ return [...groups].map(([name, n]) => ({ name, count: n })).sort((a, b) => b.count - a.count);
2809
+ }
2810
+ async function collectRuntimeDailyReport(client, options = {}) {
2811
+ const maxPages = options.maxPages ?? 10;
2812
+ if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 100) {
2813
+ throw new Error("maxPages must be an integer from 1 to 100");
2814
+ }
2815
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
2816
+ const started = now();
2817
+ const end = started.toISOString();
2818
+ const start = new Date(started.getTime() - DAY_MS).toISOString();
2819
+ const activitySource = source(
2820
+ "Daily application activity",
2821
+ "dashboard.applicationsOverview",
2822
+ "Rolling 1 day"
2823
+ );
2824
+ const profileSource = source(
2825
+ "Security profiles",
2826
+ "profiles.list (latest=true)",
2827
+ "Current configuration"
2828
+ );
2829
+ const appSource = source("Registered applications", "customerApps.list", "Current configuration");
2830
+ const logSource = source(
2831
+ "Scan log detail",
2832
+ "scanLogs.query (read-only POST)",
2833
+ "Rolling 24 hours"
2834
+ );
2835
+ let pageToken;
2836
+ const seenTokens = /* @__PURE__ */ new Set();
2837
+ const [applications, profileRows, registered, rawLogs] = await Promise.all([
2838
+ collect(
2839
+ activitySource,
2840
+ maxPages,
2841
+ async (offset) => {
2842
+ const page = await client.dashboard.applicationsOverview({
2843
+ timeInterval: 1,
2844
+ timeUnit: "day",
2845
+ limit: PAGE_SIZE,
2846
+ offset
2847
+ });
2848
+ const size = page.items?.length ?? 0;
2849
+ const totalItems = count(page.pagination?.total_items);
2850
+ if (totalItems !== null && totalItems > offset + size && size === 0) {
2851
+ throw new ReportDataError("The API returned an empty page before its advertised total.");
2852
+ }
2853
+ return {
2854
+ items: page.items,
2855
+ next: totalItems !== null ? offset + size < totalItems ? offset + size : void 0 : size === PAGE_SIZE ? offset + size : void 0
2856
+ };
2857
+ },
2858
+ (item) => JSON.stringify([item.id, item.name])
2859
+ ),
2860
+ collect(
2861
+ profileSource,
2862
+ maxPages,
2863
+ async (offset) => {
2864
+ const page = await client.profiles.list({ latest: true, limit: PAGE_SIZE, offset });
2865
+ return {
2866
+ items: page.ai_profiles,
2867
+ next: inventoryNext(page.next_offset, page.ai_profiles.length, offset)
2868
+ };
2869
+ },
2870
+ (item) => JSON.stringify([item.profile_name, item.revision])
2871
+ ),
2872
+ collect(
2873
+ appSource,
2874
+ maxPages,
2875
+ async (offset) => {
2876
+ const page = await client.customerApps.list({ limit: PAGE_SIZE, offset });
2877
+ return {
2878
+ items: page.customer_apps,
2879
+ next: inventoryNext(page.next_offset, page.customer_apps?.length ?? 0, offset)
2880
+ };
2881
+ },
2882
+ (item) => item.customer_appId
2883
+ ),
2884
+ collect(
2885
+ logSource,
2886
+ maxPages,
2887
+ async (offset) => {
2888
+ const pageNumber = offset + 1;
2889
+ const page = await client.scanLogs.query({
2890
+ time_interval: 24,
2891
+ time_unit: "hours",
2892
+ pageNumber,
2893
+ pageSize: PAGE_SIZE,
2894
+ filter: "all",
2895
+ page_token: pageToken
2896
+ });
2897
+ const items = page.scan_result_for_dashboard?.scan_result_entries;
2898
+ if (page.page_number !== void 0 && page.page_number !== pageNumber) {
2899
+ throw new ReportDataError(
2900
+ "The scan-log response page number did not match the requested page."
2901
+ );
2902
+ }
2903
+ const totalPages = count(page.total_pages);
2904
+ let next;
2905
+ if (totalPages !== null) next = pageNumber < totalPages ? offset + 1 : void 0;
2906
+ else if (page.page_token || items?.length === PAGE_SIZE) next = offset + 1;
2907
+ if (next !== void 0 && page.page_token) {
2908
+ if (seenTokens.has(page.page_token))
2909
+ throw new ReportDataError(
2910
+ "The scan-log continuation token repeated; collection stopped safely."
2911
+ );
2912
+ seenTokens.add(page.page_token);
2913
+ pageToken = page.page_token;
2914
+ }
2915
+ return { items, next };
2916
+ },
2917
+ (item) => JSON.stringify([item.scan_id, item.scan_sub_req_id])
2918
+ )
2919
+ ]);
2920
+ const findings = [];
2921
+ const add = (priority, title, evidence, recommendation, evidenceSource) => {
2922
+ findings.push({ priority, title, evidence, recommendation, source: evidenceSource });
2923
+ };
2924
+ for (const s of [activitySource, profileSource, appSource, logSource]) {
2925
+ if (s.status !== "complete")
2926
+ add(
2927
+ "review",
2928
+ `${s.name}: ${s.status}`,
2929
+ s.notes.join(" "),
2930
+ "Restore access or retry collection; do not treat missing data as zero. Increase --max-pages if the page budget was reached.",
2931
+ s.name
2932
+ );
2933
+ }
2934
+ const appIds = new Set(registered.map((app) => app.customer_appId));
2935
+ const reportApps = applications.map((app) => ({
2936
+ name: label(app.name),
2937
+ sessions: count(app.sessions_total),
2938
+ violatingSessions: count(app.sessions_violated),
2939
+ registered: app.id && appIds.has(app.id) ? true : appSource.status === "complete" && app.id ? false : null
2940
+ })).sort(
2941
+ (a, b) => (b.violatingSessions ?? -1) - (a.violatingSessions ?? -1) || a.name.localeCompare(b.name)
2942
+ );
2943
+ const sessions = activitySource.status === "unavailable" ? null : total(reportApps.map((app) => app.sessions));
2944
+ const violatingSessions = activitySource.status === "unavailable" ? null : total(reportApps.map((app) => app.violatingSessions));
2945
+ const inconsistent = reportApps.some(
2946
+ (app) => app.sessions !== null && app.violatingSessions !== null && app.violatingSessions > app.sessions
2947
+ );
2948
+ if (inconsistent || reportApps.some((app) => app.sessions === null || app.violatingSessions === null)) {
2949
+ add(
2950
+ "review",
2951
+ "Activity counters need verification",
2952
+ "Some application counters are absent, invalid, or report more violating sessions than total sessions.",
2953
+ "Compare these buckets with the SCM dashboard. A violation rate is withheld when counters are inconsistent.",
2954
+ activitySource.name
2955
+ );
2956
+ }
2957
+ if ((violatingSessions ?? 0) > 0)
2958
+ add(
2959
+ "attention",
2960
+ "Violating sessions observed",
2961
+ `${violatingSessions} violating sessions across ${reportApps.filter((app) => (app.violatingSessions ?? 0) > 0).length} application buckets in the collected daily data.`,
2962
+ "Review the highest-volume applications in SCM, confirm intended enforcement, and investigate unexpected activity. Violations are not proof of a successful attack.",
2963
+ activitySource.name
2964
+ );
2965
+ if (sessions === 0 && activitySource.status === "complete")
2966
+ add(
2967
+ "review",
2968
+ "No sessions reported in the daily window",
2969
+ "The complete application overview reports zero sessions.",
2970
+ "If traffic was expected, check the integration, application metadata, and telemetry ingestion. No activity is not proof of health.",
2971
+ activitySource.name
2972
+ );
2973
+ const latestProfiles = /* @__PURE__ */ new Map();
2974
+ for (const profile of profileRows) {
2975
+ const old = latestProfiles.get(profile.profile_name);
2976
+ if (!old || (profile.revision ?? -1) > (old.revision ?? -1))
2977
+ latestProfiles.set(profile.profile_name, profile);
2978
+ }
2979
+ const profiles = [...latestProfiles.values()].map((profile) => {
2980
+ const configs = (profile.policy?.["ai-security-profiles"] ?? []).flatMap(
2981
+ (p) => p["model-configuration"] ? [p["model-configuration"]] : []
2982
+ );
2983
+ const timeoutActions = [
2984
+ ...new Set(configs.map((c) => label(c.latency?.["inline-timeout-action"])))
2985
+ ];
2986
+ const name = label(profile.profile_name);
2987
+ if (profile.active === false)
2988
+ add(
2989
+ "review",
2990
+ `Inactive profile: ${name}`,
2991
+ "The latest returned revision is explicitly inactive.",
2992
+ "Confirm this is intentional before assigning the profile to an integration.",
2993
+ profileSource.name
2994
+ );
2995
+ if (profile.active === true && timeoutActions.some((action) => action.toLowerCase() === "allow"))
2996
+ add(
2997
+ "review",
2998
+ `Timeout allows traffic: ${name}`,
2999
+ "An active profile explicitly sets inline-timeout-action to allow.",
3000
+ "Review the availability-versus-enforcement tradeoff with the application owner; this is not evidence of a timeout or bypass.",
3001
+ profileSource.name
3002
+ );
3003
+ const masking = configs.map((c) => c["mask-data-in-storage"]);
3004
+ const storageMasking = masking.includes(false) ? "Off in at least one configuration" : masking.length > 0 && masking.every((v) => v === true) ? "On in all returned configurations" : "Unknown";
3005
+ if (profile.active === true && masking.includes(false))
3006
+ add(
3007
+ "review",
3008
+ `Storage masking disabled: ${name}`,
3009
+ "An active profile explicitly disables mask-data-in-storage in a model configuration.",
3010
+ "Confirm retention and privacy requirements. This flag alone does not establish that sensitive data was stored.",
3011
+ profileSource.name
3012
+ );
3013
+ return {
3014
+ name,
3015
+ revision: count(profile.revision),
3016
+ active: profile.active ?? null,
3017
+ modified: profile.last_modified_ts && Number.isFinite(Date.parse(profile.last_modified_ts)) ? new Date(profile.last_modified_ts).toISOString() : null,
3018
+ timeoutActions,
3019
+ storageMasking
3020
+ };
3021
+ }).sort((a, b) => a.name.localeCompare(b.name));
3022
+ if (profiles.length === 0 && profileSource.status === "complete")
3023
+ add(
3024
+ "attention",
3025
+ "No security profiles returned",
3026
+ "The current profile inventory is empty.",
3027
+ "Verify tenant scope and configure a security profile before routing application scans.",
3028
+ profileSource.name
3029
+ );
3030
+ let missingTimestamps = 0;
3031
+ let outsideWindow = 0;
3032
+ const logs = rawLogs.filter((row) => {
3033
+ const timestamp = Date.parse(row.received_ts ?? "");
3034
+ if (!Number.isFinite(timestamp)) {
3035
+ missingTimestamps++;
3036
+ return false;
3037
+ }
3038
+ if (timestamp < Date.parse(start) || timestamp >= Date.parse(end)) {
3039
+ outsideWindow++;
3040
+ return false;
3041
+ }
3042
+ return true;
3043
+ });
3044
+ if (missingTimestamps || outsideWindow)
3045
+ add(
3046
+ "review",
3047
+ "Some log records excluded from daily analysis",
3048
+ `${missingTimestamps} records lack a valid received timestamp; ${outsideWindow} fall outside the anchored UTC window.`,
3049
+ "Verify ingestion timestamps and collection-window drift before using log counts as daily totals.",
3050
+ logSource.name
3051
+ );
3052
+ const failedLogs = logs.filter(
3053
+ (row) => ["failed", "error", "timeout"].includes(row.status?.trim().toLowerCase() ?? "")
3054
+ ).length;
3055
+ if (failedLogs)
3056
+ add(
3057
+ "attention",
3058
+ "Scan processing failures observed",
3059
+ `${failedLogs} timestamp-eligible collected log entries explicitly report failed, error, or timeout status.`,
3060
+ "Review these failures in SCM, check integration error handling, and verify whether the affected application fails open or closed. This is not a tenant-wide failure rate.",
3061
+ logSource.name
3062
+ );
3063
+ const priorities = { attention: 0, review: 1, info: 2 };
3064
+ findings.sort((left, right) => priorities[left.priority] - priorities[right.priority]);
3065
+ return {
3066
+ schemaVersion: 1,
3067
+ product: "Prisma AIRS AI Runtime Security",
3068
+ title: label(options.title ?? "Daily environment report"),
3069
+ generatedAt: now().toISOString(),
3070
+ collectionStartedAt: end,
3071
+ window: {
3072
+ start,
3073
+ end,
3074
+ description: "Rolling last 24 hours, UTC (approximate for server-relative application queries)"
3075
+ },
3076
+ health: [activitySource, profileSource, appSource, logSource].every(
3077
+ (s) => s.status === "unavailable"
3078
+ ) ? "unknown" : findings.some((f) => f.priority === "attention") ? "attention" : findings.some((f) => f.priority === "review") ? "review" : "no-findings",
3079
+ sources: [activitySource, profileSource, appSource, logSource],
3080
+ findings,
3081
+ activity: {
3082
+ sessions,
3083
+ violatingSessions,
3084
+ violationRate: !inconsistent && sessions !== null && sessions > 0 && violatingSessions !== null ? violatingSessions / sessions * 100 : null,
3085
+ applications: reportApps
3086
+ },
3087
+ profiles,
3088
+ registeredApps: registered.map((app) => ({
3089
+ name: label(app.app_name),
3090
+ environment: label(app.environment),
3091
+ cloud: label(app.cloud_provider),
3092
+ model: label(app.model_name),
3093
+ keyAssociations: app.api_keys_dp_info ? app.api_keys_dp_info.length : null
3094
+ })).sort((a, b) => a.name.localeCompare(b.name)),
3095
+ logs: {
3096
+ entries: logs.length,
3097
+ actions: distribution(logs, "action"),
3098
+ verdicts: distribution(logs, "verdict"),
3099
+ tokens: logSource.status === "unavailable" ? null : total(logs.map((row) => count(row.tokens))),
3100
+ missingTimestamps,
3101
+ outsideWindow
3102
+ },
3103
+ limitations: [
3104
+ "This is a read-only operational review, not an uptime SLA, compliance attestation, or proof that attacks succeeded or were blocked. No health score is invented.",
3105
+ "Application activity uses the API\u2019s rolling one-day window; each paginated request evaluates its own server-relative window. Collection is not a transactionally consistent historical snapshot. Ingestion may lag.",
3106
+ "Sessions, scan-log entries, text records, and API calls are different units. They are not added together. Log counters describe only collected, timestamp-eligible entries; absent data is unknown.",
3107
+ "Application buckets are keyed by registered application ID plus the literal scan metadata.app_name, which can differ from registered names. Do not sum session counts as unique users or unique tenant-wide sessions.",
3108
+ "Current configuration is not a configuration-change audit. Missing policy settings are unknown, not disabled. Inactive profiles can be intentional; current profile state may differ from the revision used by a historical scan.",
3109
+ "Per-app token summaries and detector/severity breakdowns require 7/30/60-day windows, so they are not presented as daily metrics. No previous-day comparison or trend is inferred.",
3110
+ "Prompts, responses, user identities/IPs, API keys, auth codes, tenant IDs, and raw errors are omitted. Application/profile names and configuration metadata remain confidential; review before sharing."
3111
+ ]
3112
+ };
3113
+ }
3114
+
2484
3115
  export {
2485
3116
  SdkManagementService,
2486
3117
  getOrCreateManagementClient,
@@ -2505,5 +3136,9 @@ export {
2505
3136
  validateExamples,
2506
3137
  validateTopic,
2507
3138
  computeMetrics,
2508
- computeCategoryBreakdown
3139
+ computeCategoryBreakdown,
3140
+ writeReportFile,
3141
+ renderRuntimeReportMarkdown,
3142
+ renderRuntimeReportHtml,
3143
+ collectRuntimeDailyReport
2509
3144
  };
package/dist/cli/index.js CHANGED
@@ -9,18 +9,22 @@ import {
9
9
  SdkPromptSetService,
10
10
  SdkRedTeamService,
11
11
  SdkRuntimeService,
12
+ collectRuntimeDailyReport,
12
13
  computeMetrics,
13
14
  getOrCreateManagementClient,
14
15
  inspectConfig,
15
16
  loadConfig,
16
17
  readBackupDir,
17
18
  readBackupFile,
19
+ renderRuntimeReportHtml,
20
+ renderRuntimeReportMarkdown,
18
21
  resolveConfigFilePath,
19
22
  resolveOutputDir,
20
23
  sanitizeFilename,
21
24
  validateTopic,
22
- writeBackupFile
23
- } from "../chunk-LF6JPUA4.js";
25
+ writeBackupFile,
26
+ writeReportFile
27
+ } from "../chunk-NWK7RG2N.js";
24
28
 
25
29
  // src/cli/index.ts
26
30
  import "dotenv/config";
@@ -39,8 +43,8 @@ function installProcessGuards() {
39
43
  }
40
44
 
41
45
  // src/cli/program.ts
46
+ import { randomUUID as randomUUID6 } from "crypto";
42
47
  import { readFileSync as readFileSync4 } from "fs";
43
- import { homedir } from "os";
44
48
  import { dirname as dirname4, join as join4 } from "path";
45
49
  import { fileURLToPath } from "url";
46
50
  import { Command } from "commander";
@@ -3085,8 +3089,8 @@ function registerAiGatewayInference(parent) {
3085
3089
  process.once("SIGINT", interrupt);
3086
3090
  process.once("SIGTERM", interrupt);
3087
3091
  process.stdout.on("error", outputError);
3088
- const write = (text) => new Promise((resolve2, reject) => {
3089
- process.stdout.write(text, (error) => error ? reject(error) : resolve2());
3092
+ const write = (text) => new Promise((resolve3, reject) => {
3093
+ process.stdout.write(text, (error) => error ? reject(error) : resolve3());
3090
3094
  });
3091
3095
  const requestOptions = { signal: controller.signal };
3092
3096
  const result = kind === "chat" ? await client.createChatCompletion(
@@ -3197,7 +3201,7 @@ function isAirsUrl(url) {
3197
3201
  }
3198
3202
  }
3199
3203
  var MASK = "***";
3200
- var SENSITIVE_KEY_PATTERN = /token|secret|password|passwd|credential|authorization|cookie|api[-_]?key|client[-_]?auth|^key$/i;
3204
+ var SENSITIVE_KEY_PATTERN = /token|secret|password|passwd|credential|authorization|cookie|api[-_]?key|client[-_]?auth|auth[-_]?code|^key$/i;
3201
3205
  function isSensitiveKey(key) {
3202
3206
  return SENSITIVE_KEY_PATTERN.test(key);
3203
3207
  }
@@ -3236,28 +3240,6 @@ function redactUrl(url) {
3236
3240
  return url;
3237
3241
  }
3238
3242
  }
3239
- function pruneDebugLogs(dir, keep) {
3240
- let files;
3241
- try {
3242
- files = readdirSync(dir).filter((f) => f.startsWith("debug-api-") && f.endsWith(".jsonl"));
3243
- } catch {
3244
- return;
3245
- }
3246
- const byAge = files.map((f) => {
3247
- const path3 = join(dir, f);
3248
- try {
3249
- return { path: path3, mtime: statSync(path3).mtimeMs };
3250
- } catch {
3251
- return null;
3252
- }
3253
- }).filter((e) => e !== null).sort((a, b) => b.mtime - a.mtime);
3254
- for (const { path: path3 } of byAge.slice(keep)) {
3255
- try {
3256
- unlinkSync(path3);
3257
- } catch {
3258
- }
3259
- }
3260
- }
3261
3243
  function headersToRecord(headers) {
3262
3244
  if (!headers) return {};
3263
3245
  if (typeof headers === "object" && "forEach" in headers && typeof headers.forEach === "function") {
@@ -3272,11 +3254,9 @@ function headersToRecord(headers) {
3272
3254
  }
3273
3255
  return headers;
3274
3256
  }
3275
- var KEEP_DEBUG_LOGS = 10;
3276
3257
  function installDebugLogger(logPath) {
3277
3258
  mkdirSync(dirname(logPath), { recursive: true, mode: 448 });
3278
- writeFileSync(logPath, "", { encoding: "utf8", mode: 384 });
3279
- pruneDebugLogs(dirname(logPath), KEEP_DEBUG_LOGS);
3259
+ writeFileSync(logPath, "", { encoding: "utf8", mode: 384, flag: "wx" });
3280
3260
  const originalFetch = globalThis.fetch;
3281
3261
  globalThis.fetch = async function debugFetch(input, init2) {
3282
3262
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
@@ -3298,7 +3278,8 @@ function installDebugLogger(logPath) {
3298
3278
  try {
3299
3279
  reqBody = redactDeep(JSON.parse(String(init2.body)));
3300
3280
  } catch {
3301
- reqBody = String(init2.body);
3281
+ const contentType = new Headers(rawHeaders).get("content-type") ?? "";
3282
+ reqBody = contentType.includes("application/x-www-form-urlencoded") ? redactDeep(Object.fromEntries(new URLSearchParams(String(init2.body)))) : "[NON-JSON BODY OMITTED]";
3302
3283
  }
3303
3284
  }
3304
3285
  const ts2 = (/* @__PURE__ */ new Date()).toISOString();
@@ -3309,7 +3290,7 @@ function installDebugLogger(logPath) {
3309
3290
  try {
3310
3291
  response = await originalFetch(input, init2);
3311
3292
  } catch (err) {
3312
- error = inference ? "Runtime request failed" : err instanceof Error ? err.message : String(err);
3293
+ error = inference ? "Runtime request failed" : "Request failed";
3313
3294
  const entry2 = JSON.stringify({
3314
3295
  timestamp: ts2,
3315
3296
  durationMs: Date.now() - startMs,
@@ -3334,7 +3315,7 @@ function installDebugLogger(logPath) {
3334
3315
  try {
3335
3316
  resBody = redactDeep(JSON.parse(text));
3336
3317
  } catch {
3337
- resBody = text;
3318
+ resBody = "[NON-JSON BODY OMITTED]";
3338
3319
  }
3339
3320
  } catch {
3340
3321
  resBody = "<unreadable>";
@@ -5350,8 +5331,8 @@ async function withTimeout(promise, ms) {
5350
5331
  try {
5351
5332
  return await Promise.race([
5352
5333
  promise,
5353
- new Promise((resolve2) => {
5354
- timer = setTimeout(() => resolve2(TIMED_OUT), ms);
5334
+ new Promise((resolve3) => {
5335
+ timer = setTimeout(() => resolve3(TIMED_OUT), ms);
5355
5336
  })
5356
5337
  ]);
5357
5338
  } finally {
@@ -5621,12 +5602,12 @@ function hasBin(bin) {
5621
5602
  }
5622
5603
  }
5623
5604
  function run(bin, args, label) {
5624
- return new Promise((resolve2, reject) => {
5605
+ return new Promise((resolve3, reject) => {
5625
5606
  const child = spawn(bin, args, { stdio: "inherit" });
5626
5607
  child.on("error", (err) => reject(new Error(`Failed to start ${bin}: ${err.message}`)));
5627
5608
  child.on("close", (code) => {
5628
5609
  if (code !== 0) reject(new Error(`${label} failed with exit code ${code}`));
5629
- else resolve2();
5610
+ else resolve3();
5630
5611
  });
5631
5612
  });
5632
5613
  }
@@ -7439,7 +7420,7 @@ function registerRedteamCommand(program) {
7439
7420
  }
7440
7421
 
7441
7422
  // src/cli/commands/runtime.ts
7442
- import { randomUUID as randomUUID4 } from "crypto";
7423
+ import { randomUUID as randomUUID5 } from "crypto";
7443
7424
  import * as fs5 from "fs";
7444
7425
  import { readFile as readFile10 } from "fs/promises";
7445
7426
  import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
@@ -8994,6 +8975,76 @@ function registerCleanupCommand(parent) {
8994
8975
  });
8995
8976
  }
8996
8977
 
8978
+ // src/cli/commands/runtime-report.ts
8979
+ import { lstat } from "fs/promises";
8980
+ import { resolve as resolve2 } from "path";
8981
+ import { randomUUID as randomUUID4 } from "crypto";
8982
+ function registerRuntimeReportCommand(runtime) {
8983
+ 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(
8984
+ "--output-file <path>",
8985
+ "Destination (default: new timestamped file in CWD); - for stdout; never overwrite"
8986
+ ).option("--title <text>", "Report title", "Daily environment report").option("--max-pages <n>", "Page budget per source, 1\u2013100 (100 records/page)", "10").option("--strict", "Exit 1 after writing if any source is incomplete").addHelpText(
8987
+ "after",
8988
+ examples(
8989
+ "airs runtime report --output-file ./airs-daily.html",
8990
+ "airs runtime report --output markdown --output-file ./airs-daily.md",
8991
+ "airs runtime report",
8992
+ "airs runtime report --strict --max-pages 20 --output-file - > daily.html"
8993
+ )
8994
+ ).action(async (opts) => {
8995
+ const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
8996
+ if (format !== "html" && format !== "markdown")
8997
+ usageError("Report output must be html or markdown");
8998
+ const maxPages = Number(opts.maxPages);
8999
+ if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages) || maxPages > 100)
9000
+ usageError("--max-pages must be an integer from 1 to 100");
9001
+ if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
9002
+ usageError("--title must contain 1\u2013240 characters");
9003
+ if (opts.outputFile !== void 0 && !opts.outputFile.trim())
9004
+ usageError("--output-file cannot be empty");
9005
+ const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID4().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
9006
+ const outputPath = opts.outputFile === "-" ? void 0 : resolve2(opts.outputFile ?? defaultName);
9007
+ try {
9008
+ if (outputPath) {
9009
+ const existing = await lstat(outputPath).catch((error) => {
9010
+ if (error.code !== "ENOENT") throw error;
9011
+ return void 0;
9012
+ });
9013
+ if (existing) throw new Error("Output already exists");
9014
+ }
9015
+ const config = await loadConfig();
9016
+ const client = getOrCreateManagementClient({
9017
+ clientId: config.mgmtClientId,
9018
+ clientSecret: config.mgmtClientSecret,
9019
+ tsgId: config.mgmtTsgId,
9020
+ apiEndpoint: config.mgmtEndpoint,
9021
+ tokenEndpoint: config.mgmtTokenEndpoint,
9022
+ numRetries: 0
9023
+ });
9024
+ ui.status("Collecting daily runtime activity and current configuration (read-only)...");
9025
+ const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages });
9026
+ const rendered = format === "html" ? renderRuntimeReportHtml(report) : renderRuntimeReportMarkdown(report);
9027
+ if (outputPath) {
9028
+ await writeReportFile(outputPath, rendered);
9029
+ ui.status(`Report \u2192 ${outputPath.replace(/[\p{Cc}\p{Cf}]/gu, "_")} (mode 0600)`);
9030
+ } else {
9031
+ await new Promise(
9032
+ (done, reject) => process.stdout.write(rendered, (error) => error ? reject(error) : done())
9033
+ );
9034
+ }
9035
+ const incomplete = report.sources.filter((source) => source.status !== "complete").length;
9036
+ if (incomplete)
9037
+ ui.status(`${incomplete} source(s) incomplete; review the evidence section.`);
9038
+ if (report.health === "unknown" || opts.strict && incomplete > 0) process.exitCode = 1;
9039
+ } catch {
9040
+ ui.error(
9041
+ "Report generation failed. Verify Management API configuration and a writable, new output path. Existing files are never overwritten."
9042
+ );
9043
+ process.exitCode = 1;
9044
+ }
9045
+ });
9046
+ }
9047
+
8997
9048
  // src/cli/commands/topics-apply.ts
8998
9049
  async function applyTopicToProfile(mgmt, input) {
8999
9050
  const allTopics = await mgmt.listTopics();
@@ -9446,7 +9497,7 @@ function completedBulkResults(state) {
9446
9497
  }
9447
9498
  async function writeBulkResults(outputPath, results) {
9448
9499
  await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
9449
- const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID4()}`;
9500
+ const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID5()}`;
9450
9501
  try {
9451
9502
  await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
9452
9503
  encoding: "utf-8",
@@ -9481,6 +9532,7 @@ async function createMgmtService() {
9481
9532
  }
9482
9533
  function registerRuntimeCommand(program) {
9483
9534
  const runtime = program.command("runtime").description("Runtime prompt scanning against AIRS profiles");
9535
+ registerRuntimeReportCommand(runtime);
9484
9536
  const apiKeys = runtime.command("api-keys").description("Manage AIRS API keys");
9485
9537
  const apiKeysList = registerListFlags(apiKeys.command("list"), { dialect: "offset" }).description("List API keys").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (opts) => {
9486
9538
  try {
@@ -10306,7 +10358,12 @@ function buildProgram() {
10306
10358
  program.hook("preAction", async (_thisCommand, actionCommand) => {
10307
10359
  const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
10308
10360
  setQuiet(Boolean(root.quiet));
10309
- if (READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
10361
+ const isRuntimeReport = actionCommand.name() === "report" && actionCommand.parent?.name() === "runtime";
10362
+ if (isRuntimeReport && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
10363
+ usageError(
10364
+ "Disable --debug and PANW_AI_SEC_DEBUG for environment reports to avoid persisting scan-log content"
10365
+ );
10366
+ if (!isRuntimeReport && READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
10310
10367
  try {
10311
10368
  const format = await resolveOutput(actionCommand, actionCommand.opts());
10312
10369
  actionCommand.setOptionValueWithSource("output", format, "implied");
@@ -10315,8 +10372,18 @@ function buildProgram() {
10315
10372
  }
10316
10373
  }
10317
10374
  if (root.debug) {
10318
- const logPath = join4(homedir(), ".prisma-airs", `debug-api-${Date.now()}.jsonl`);
10319
- installDebugLogger(logPath);
10375
+ const logPath = join4(
10376
+ process.cwd(),
10377
+ `debug-api-${Date.now()}-${randomUUID6().slice(0, 8)}.jsonl`
10378
+ );
10379
+ try {
10380
+ installDebugLogger(logPath);
10381
+ } catch {
10382
+ ui.error(
10383
+ "Cannot create the debug log in the current working directory. Run from a writable directory or omit --debug."
10384
+ );
10385
+ process.exit(1);
10386
+ }
10320
10387
  ui.status(`Debug: API log \u2192 ${logPath}`);
10321
10388
  }
10322
10389
  });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CreateCustomTopicRequest, CustomTopic as CustomTopic$1, CreateSecurityProfileRequest, ManagementClientOptions, ModelSecurityClientOptions, RedTeamClientOptions, InitOptions } from '@cdot65/prisma-airs-sdk';
1
+ import { CreateCustomTopicRequest, CustomTopic as CustomTopic$1, CreateSecurityProfileRequest, ManagementClientOptions, ModelSecurityClientOptions, RedTeamClientOptions, InitOptions, ManagementClient } from '@cdot65/prisma-airs-sdk';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**
@@ -1842,4 +1842,113 @@ declare function computeMetrics(results: TestResult[]): EfficacyMetrics;
1842
1842
  /** Compute per-category error breakdown from test results. Sorted by error rate descending. */
1843
1843
  declare function computeCategoryBreakdown(results: TestResult[]): CategoryBreakdown[];
1844
1844
 
1845
- 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 ResourceType, type RestoreResult, type RunState, 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, computeCategoryBreakdown, computeMetrics, loadConfig, readBackupDir, readBackupFile, resolveOutputDir, sanitizeFilename, validateDescription, validateExamples, validateName, validateTopic, writeBackupFile };
1845
+ /** Atomically publish a private report without overwriting a file or following a symlink. */
1846
+ declare function writeReportFile(path: string, content: string): Promise<void>;
1847
+
1848
+ /** Read-only SDK surface needed by the first product report. */
1849
+ interface RuntimeReportClient {
1850
+ dashboard: Pick<ManagementClient['dashboard'], 'applicationsOverview'>;
1851
+ profiles: Pick<ManagementClient['profiles'], 'list'>;
1852
+ customerApps: Pick<ManagementClient['customerApps'], 'list'>;
1853
+ scanLogs: Pick<ManagementClient['scanLogs'], 'query'>;
1854
+ }
1855
+ type ReportSourceStatus = 'complete' | 'partial' | 'unavailable';
1856
+ type ReportPriority = 'attention' | 'review' | 'info';
1857
+ type ReportFormat = 'html' | 'markdown';
1858
+ interface ReportSource {
1859
+ name: string;
1860
+ method: string;
1861
+ window: string;
1862
+ status: ReportSourceStatus;
1863
+ records: number;
1864
+ pages: number;
1865
+ notes: string[];
1866
+ }
1867
+ interface ReportFinding {
1868
+ priority: ReportPriority;
1869
+ title: string;
1870
+ evidence: string;
1871
+ recommendation: string;
1872
+ source: string;
1873
+ }
1874
+ interface ReportApplication {
1875
+ name: string;
1876
+ sessions: number | null;
1877
+ violatingSessions: number | null;
1878
+ registered: boolean | null;
1879
+ }
1880
+ interface ReportProfile {
1881
+ name: string;
1882
+ revision: number | null;
1883
+ active: boolean | null;
1884
+ modified: string | null;
1885
+ timeoutActions: string[];
1886
+ storageMasking: string;
1887
+ }
1888
+ interface ReportRegisteredApp {
1889
+ name: string;
1890
+ environment: string;
1891
+ cloud: string;
1892
+ model: string;
1893
+ keyAssociations: number | null;
1894
+ }
1895
+ interface ReportLogSummary {
1896
+ entries: number;
1897
+ actions: Array<{
1898
+ name: string;
1899
+ count: number;
1900
+ }>;
1901
+ verdicts: Array<{
1902
+ name: string;
1903
+ count: number;
1904
+ }>;
1905
+ tokens: number | null;
1906
+ missingTimestamps: number;
1907
+ outsideWindow: number;
1908
+ }
1909
+ /** Allowlisted, credential-free projection; never contains raw responses or scan content. */
1910
+ interface RuntimeDailyReport {
1911
+ schemaVersion: 1;
1912
+ product: 'Prisma AIRS AI Runtime Security';
1913
+ title: string;
1914
+ generatedAt: string;
1915
+ collectionStartedAt: string;
1916
+ window: {
1917
+ start: string;
1918
+ end: string;
1919
+ description: string;
1920
+ };
1921
+ health: 'attention' | 'review' | 'no-findings' | 'unknown';
1922
+ sources: ReportSource[];
1923
+ findings: ReportFinding[];
1924
+ activity: {
1925
+ sessions: number | null;
1926
+ violatingSessions: number | null;
1927
+ violationRate: number | null;
1928
+ applications: ReportApplication[];
1929
+ };
1930
+ profiles: ReportProfile[];
1931
+ registeredApps: ReportRegisteredApp[];
1932
+ logs: ReportLogSummary;
1933
+ limitations: string[];
1934
+ }
1935
+ interface RuntimeReportOptions {
1936
+ title?: string;
1937
+ /** Per-source page budget (1–100); partial results are explicitly marked. Default 10. */
1938
+ maxPages?: number;
1939
+ /** Inject a clock for deterministic tests. */
1940
+ now?: () => Date;
1941
+ }
1942
+
1943
+ /** Render the same evidence model as a portable, inert Markdown deliverable. */
1944
+ declare function renderRuntimeReportMarkdown(report: RuntimeDailyReport): string;
1945
+ /** Standalone HTML: inline CSS/JS, no dependencies, external requests, or embedded raw data. */
1946
+ declare function renderRuntimeReportHtml(report: RuntimeDailyReport): string;
1947
+
1948
+ /**
1949
+ * Build a daily environment report through SDK reads only. No scan submissions or mutations.
1950
+ * Application sessions, log entries and current inventory remain separate units of evidence.
1951
+ */
1952
+ declare function collectRuntimeDailyReport(client: RuntimeReportClient, options?: RuntimeReportOptions): Promise<RuntimeDailyReport>;
1953
+
1954
+ 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 ReportFinding, type ReportFormat, type ReportLogSummary, type ReportPriority, type ReportProfile, type ReportRegisteredApp, 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 };
package/dist/index.js CHANGED
@@ -6,19 +6,23 @@ import {
6
6
  SdkPromptSetService,
7
7
  SdkRedTeamService,
8
8
  SdkRuntimeService,
9
+ collectRuntimeDailyReport,
9
10
  computeCategoryBreakdown,
10
11
  computeMetrics,
11
12
  loadConfig,
12
13
  readBackupDir,
13
14
  readBackupFile,
15
+ renderRuntimeReportHtml,
16
+ renderRuntimeReportMarkdown,
14
17
  resolveOutputDir,
15
18
  sanitizeFilename,
16
19
  validateDescription,
17
20
  validateExamples,
18
21
  validateName,
19
22
  validateTopic,
20
- writeBackupFile
21
- } from "./chunk-LF6JPUA4.js";
23
+ writeBackupFile,
24
+ writeReportFile
25
+ } from "./chunk-NWK7RG2N.js";
22
26
  export {
23
27
  AirsScanService,
24
28
  SDK_ASYNC_BATCH_SIZE,
@@ -27,16 +31,20 @@ export {
27
31
  SdkPromptSetService,
28
32
  SdkRedTeamService,
29
33
  SdkRuntimeService,
34
+ collectRuntimeDailyReport,
30
35
  computeCategoryBreakdown,
31
36
  computeMetrics,
32
37
  loadConfig,
33
38
  readBackupDir,
34
39
  readBackupFile,
40
+ renderRuntimeReportHtml,
41
+ renderRuntimeReportMarkdown,
35
42
  resolveOutputDir,
36
43
  sanitizeFilename,
37
44
  validateDescription,
38
45
  validateExamples,
39
46
  validateName,
40
47
  validateTopic,
41
- writeBackupFile
48
+ writeBackupFile,
49
+ writeReportFile
42
50
  };
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": "4.4.0",
4
+ "version": "4.5.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",