@cdot65/prisma-airs-cli 4.4.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/{chunk-LF6JPUA4.js → chunk-IRLIFMGQ.js} +781 -16
- package/dist/cli/index.js +433 -173
- package/dist/index.d.ts +141 -2
- package/dist/index.js +11 -3
- package/package.json +2 -2
|
@@ -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
|
|
286
|
+
const total2 = response.pagination?.total_items;
|
|
287
287
|
offset += page.length;
|
|
288
|
-
if (page.length === 0 || page.length < limit ||
|
|
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
|
|
1359
|
+
const total2 = sc.total ?? successful + failed;
|
|
1360
1360
|
return {
|
|
1361
1361
|
id: sc.id,
|
|
1362
1362
|
displayName: sc.display_name,
|
|
1363
|
-
asr:
|
|
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
|
|
1686
|
+
const source2 = value ?? {};
|
|
1687
1687
|
return Object.fromEntries(
|
|
1688
|
-
RUNTIME_DETECTION_KEYS.filter((key) => typeof
|
|
1688
|
+
RUNTIME_DETECTION_KEYS.filter((key) => typeof source2[key] === "boolean").map((key) => [
|
|
1689
1689
|
key,
|
|
1690
|
-
|
|
1690
|
+
source2[key]
|
|
1691
1691
|
])
|
|
1692
1692
|
);
|
|
1693
1693
|
}
|
|
@@ -2239,6 +2239,7 @@ var ConfigSchema = z.object({
|
|
|
2239
2239
|
mgmtClientSecret: z.string().optional(),
|
|
2240
2240
|
mgmtTsgId: z.string().optional(),
|
|
2241
2241
|
mgmtEndpoint: z.string().optional(),
|
|
2242
|
+
mgmtDashboardEndpoint: z.string().url().optional(),
|
|
2242
2243
|
mgmtTokenEndpoint: z.string().optional(),
|
|
2243
2244
|
dlpEndpoint: z.string().optional(),
|
|
2244
2245
|
// Red Team (endpoints only; creds shared with mgmt*)
|
|
@@ -2281,6 +2282,7 @@ function fromEnv() {
|
|
|
2281
2282
|
mgmtClientSecret: env.PANW_MGMT_CLIENT_SECRET,
|
|
2282
2283
|
mgmtTsgId: env.PANW_MGMT_TSG_ID,
|
|
2283
2284
|
mgmtEndpoint: env.PANW_MGMT_ENDPOINT,
|
|
2285
|
+
mgmtDashboardEndpoint: env.PANW_MGMT_DASHBOARD_ENDPOINT,
|
|
2284
2286
|
mgmtTokenEndpoint: env.PANW_MGMT_TOKEN_ENDPOINT,
|
|
2285
2287
|
dlpEndpoint: env.PANW_DLP_ENDPOINT,
|
|
2286
2288
|
redTeamDataEndpoint: env.PANW_RED_TEAM_DATA_ENDPOINT,
|
|
@@ -2342,8 +2344,8 @@ async function inspectConfig(configFilePath) {
|
|
|
2342
2344
|
const config = ConfigSchema.parse(merged);
|
|
2343
2345
|
const entries = {};
|
|
2344
2346
|
for (const key of Object.keys(ConfigSchema.shape)) {
|
|
2345
|
-
const
|
|
2346
|
-
entries[key] = { value: config[key], source };
|
|
2347
|
+
const source2 = key in envConfig ? "env" : key in fileConfig ? "file" : "default";
|
|
2348
|
+
entries[key] = { value: config[key], source: source2 };
|
|
2347
2349
|
}
|
|
2348
2350
|
return entries;
|
|
2349
2351
|
}
|
|
@@ -2441,10 +2443,10 @@ function computeMetrics(results) {
|
|
|
2441
2443
|
}
|
|
2442
2444
|
const totalPositives = truePositives + falseNegatives;
|
|
2443
2445
|
const totalNegatives = trueNegatives + falsePositives;
|
|
2444
|
-
const
|
|
2446
|
+
const total2 = results.length;
|
|
2445
2447
|
const truePositiveRate = totalPositives > 0 ? truePositives / totalPositives : 0;
|
|
2446
2448
|
const trueNegativeRate = totalNegatives > 0 ? trueNegatives / totalNegatives : 0;
|
|
2447
|
-
const accuracy =
|
|
2449
|
+
const accuracy = total2 > 0 ? (truePositives + trueNegatives) / total2 : 0;
|
|
2448
2450
|
const coverage = Math.min(truePositiveRate, trueNegativeRate);
|
|
2449
2451
|
const precision = truePositives + falsePositives > 0 ? truePositives / (truePositives + falsePositives) : 0;
|
|
2450
2452
|
const recall = truePositiveRate;
|
|
@@ -2472,15 +2474,774 @@ function computeCategoryBreakdown(results) {
|
|
|
2472
2474
|
if (r.testCase.expectedTriggered && !r.actualTriggered) entry.fn++;
|
|
2473
2475
|
map.set(cat, entry);
|
|
2474
2476
|
}
|
|
2475
|
-
return [...map.entries()].map(([category, { total, fp, fn }]) => ({
|
|
2477
|
+
return [...map.entries()].map(([category, { total: total2, fp, fn }]) => ({
|
|
2476
2478
|
category,
|
|
2477
|
-
total,
|
|
2479
|
+
total: total2,
|
|
2478
2480
|
fp,
|
|
2479
2481
|
fn,
|
|
2480
|
-
errorRate:
|
|
2482
|
+
errorRate: total2 > 0 ? (fp + fn) / total2 : 0
|
|
2481
2483
|
})).sort((a, b) => b.errorRate - a.errorRate);
|
|
2482
2484
|
}
|
|
2483
2485
|
|
|
2486
|
+
// src/reports/io.ts
|
|
2487
|
+
import { randomUUID } from "crypto";
|
|
2488
|
+
import { link, open, unlink } from "fs/promises";
|
|
2489
|
+
import { dirname, join as join2 } from "path";
|
|
2490
|
+
async function writeReportFile(path2, content) {
|
|
2491
|
+
const temporary = join2(dirname(path2), `.airs-report-${randomUUID()}.tmp`);
|
|
2492
|
+
const file = await open(temporary, "wx", 384);
|
|
2493
|
+
try {
|
|
2494
|
+
await file.writeFile(content, "utf8");
|
|
2495
|
+
await file.sync();
|
|
2496
|
+
await file.close();
|
|
2497
|
+
await link(temporary, path2);
|
|
2498
|
+
} finally {
|
|
2499
|
+
await file.close();
|
|
2500
|
+
await unlink(temporary);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// src/reports/render.ts
|
|
2505
|
+
import { createHash } from "crypto";
|
|
2506
|
+
var HEALTH = {
|
|
2507
|
+
attention: "Attention required",
|
|
2508
|
+
review: "Review required",
|
|
2509
|
+
"no-findings": "No findings in available evidence",
|
|
2510
|
+
unknown: "Unable to assess"
|
|
2511
|
+
};
|
|
2512
|
+
function html(value) {
|
|
2513
|
+
return String(value ?? "Unknown").replace(
|
|
2514
|
+
/[&<>"']/g,
|
|
2515
|
+
(char) => ({
|
|
2516
|
+
"&": "&",
|
|
2517
|
+
"<": "<",
|
|
2518
|
+
">": ">",
|
|
2519
|
+
'"': """,
|
|
2520
|
+
"'": "'"
|
|
2521
|
+
})[char] ?? char
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
function md(value) {
|
|
2525
|
+
return String(value ?? "Unknown").replace(
|
|
2526
|
+
/[&<>"'\\`*_[\]{}()|!#~\r\n]/g,
|
|
2527
|
+
(char) => char === "\r" || char === "\n" ? " " : `&#${char.charCodeAt(0)};`
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
function number(value) {
|
|
2531
|
+
return value === null ? "Unknown" : value.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
2532
|
+
}
|
|
2533
|
+
function tables(report) {
|
|
2534
|
+
const sessionSource = report.sources.find((source2) => source2.name === "Daily session inventory");
|
|
2535
|
+
return [
|
|
2536
|
+
{
|
|
2537
|
+
title: "Daily application activity",
|
|
2538
|
+
id: "applications",
|
|
2539
|
+
headers: [
|
|
2540
|
+
"Application bucket",
|
|
2541
|
+
"Sessions",
|
|
2542
|
+
"Violating sessions",
|
|
2543
|
+
"Violation rate",
|
|
2544
|
+
"Registered ID match"
|
|
2545
|
+
],
|
|
2546
|
+
rows: report.activity.applications.map((app) => [
|
|
2547
|
+
app.name,
|
|
2548
|
+
number(app.sessions),
|
|
2549
|
+
number(app.violatingSessions),
|
|
2550
|
+
app.sessions !== null && app.sessions > 0 && app.violatingSessions !== null && app.violatingSessions <= app.sessions ? `${number(app.violatingSessions / app.sessions * 100)}%` : "Unknown",
|
|
2551
|
+
app.registered === null ? "Unknown" : app.registered ? "Yes" : "Not in returned inventory"
|
|
2552
|
+
]),
|
|
2553
|
+
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."
|
|
2554
|
+
},
|
|
2555
|
+
{
|
|
2556
|
+
title: "Current security profiles",
|
|
2557
|
+
id: "profiles",
|
|
2558
|
+
headers: [
|
|
2559
|
+
"Profile",
|
|
2560
|
+
"Revision",
|
|
2561
|
+
"Active",
|
|
2562
|
+
"Timeout action",
|
|
2563
|
+
"Storage masking",
|
|
2564
|
+
"Last modified (UTC)"
|
|
2565
|
+
],
|
|
2566
|
+
rows: report.profiles.map((profile) => [
|
|
2567
|
+
profile.name,
|
|
2568
|
+
number(profile.revision),
|
|
2569
|
+
profile.active === null ? "Unknown" : profile.active ? "Yes" : "No",
|
|
2570
|
+
profile.timeoutActions.join(", ") || "Unknown",
|
|
2571
|
+
profile.storageMasking,
|
|
2572
|
+
profile.modified
|
|
2573
|
+
]),
|
|
2574
|
+
note: "Latest returned revision per profile name. These are current settings, not a record of changes during the daily window."
|
|
2575
|
+
},
|
|
2576
|
+
{
|
|
2577
|
+
title: "Registered application inventory",
|
|
2578
|
+
id: "inventory",
|
|
2579
|
+
headers: ["Application", "Environment", "Cloud", "Model", "Key associations"],
|
|
2580
|
+
rows: report.registeredApps.map((app) => [
|
|
2581
|
+
app.name,
|
|
2582
|
+
app.environment,
|
|
2583
|
+
app.cloud,
|
|
2584
|
+
app.model,
|
|
2585
|
+
number(app.keyAssociations)
|
|
2586
|
+
]),
|
|
2587
|
+
note: "Current registered applications, separate from scan-metadata application buckets. Association counts do not establish key validity or deployment health. No key values or auth codes are included."
|
|
2588
|
+
},
|
|
2589
|
+
{
|
|
2590
|
+
title: "Collected session observations",
|
|
2591
|
+
id: "sessions",
|
|
2592
|
+
headers: ["Session status", "Entries"],
|
|
2593
|
+
rows: report.sessions.statuses.map((entry) => [entry.name, number(entry.count)]),
|
|
2594
|
+
note: `Source: ${sessionSource?.status ?? "unavailable"}. Timestamp-eligible entries: ${number(report.sessions.entries)}. Violated status: ${number(report.sessions.violatingSessions)}. Missing timestamps: ${report.sessions.missingTimestamps}; outside window: ${report.sessions.outsideWindow}. These are collected session counts, not scan actions or detector events. No scan content is fetched.`
|
|
2595
|
+
},
|
|
2596
|
+
{
|
|
2597
|
+
title: "Daily session chart",
|
|
2598
|
+
id: "session-chart",
|
|
2599
|
+
headers: ["Bucket time (UTC)", "Sessions", "Violating sessions", "Detector violations"],
|
|
2600
|
+
rows: report.dailyTelemetry.chart.buckets.map((bucket) => [
|
|
2601
|
+
bucket.time,
|
|
2602
|
+
number(bucket.sessions),
|
|
2603
|
+
number(bucket.violatingSessions),
|
|
2604
|
+
number(bucket.violations.total)
|
|
2605
|
+
]),
|
|
2606
|
+
note: `Independent chart totals: ${number(report.dailyTelemetry.chart.sessions)} sessions; ${number(report.dailyTelemetry.chart.violatingSessions)} violating sessions. Do not force these counters to equal a separately paginated inventory.`
|
|
2607
|
+
},
|
|
2608
|
+
{
|
|
2609
|
+
title: "Top applications by detector violations",
|
|
2610
|
+
id: "top-violations",
|
|
2611
|
+
headers: ["Application bucket", "Detector violations", "Detection types"],
|
|
2612
|
+
rows: report.dailyTelemetry.topApplications.map((app) => [
|
|
2613
|
+
app.name,
|
|
2614
|
+
number(app.violations),
|
|
2615
|
+
app.detectors.map((detector) => `${detector.name}: ${number(detector.count)}`).join(", ")
|
|
2616
|
+
]),
|
|
2617
|
+
note: "Server-ranked subset for one day, not a complete application inventory. Detector violations may exceed the number of violating sessions."
|
|
2618
|
+
},
|
|
2619
|
+
{
|
|
2620
|
+
title: "Daily detector severity trend",
|
|
2621
|
+
id: "violation-trend",
|
|
2622
|
+
headers: ["Bucket time (UTC)", "Critical", "High", "Medium", "Low", "Total"],
|
|
2623
|
+
rows: report.dailyTelemetry.violationTrend.map((bucket) => [
|
|
2624
|
+
bucket.time,
|
|
2625
|
+
...["critical", "high", "medium", "low", "total"].map(
|
|
2626
|
+
(key) => number(bucket.violations[key])
|
|
2627
|
+
)
|
|
2628
|
+
]),
|
|
2629
|
+
note: "API-reported detector-policy events over the rolling day. Severity counts are preserved independently from distinct session counts; no previous-day comparison is inferred."
|
|
2630
|
+
},
|
|
2631
|
+
{
|
|
2632
|
+
title: "Evidence and collection coverage",
|
|
2633
|
+
id: "evidence",
|
|
2634
|
+
headers: ["Source / SDK method", "Window", "Status", "Records", "Pages", "Collection notes"],
|
|
2635
|
+
rows: report.sources.map((source2) => [
|
|
2636
|
+
`${source2.name} \u2014 ${source2.method}`,
|
|
2637
|
+
source2.window,
|
|
2638
|
+
source2.status,
|
|
2639
|
+
number(source2.records),
|
|
2640
|
+
source2.pages,
|
|
2641
|
+
source2.notes.join(" ") || "Collection completed."
|
|
2642
|
+
]),
|
|
2643
|
+
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."
|
|
2644
|
+
}
|
|
2645
|
+
];
|
|
2646
|
+
}
|
|
2647
|
+
function metrics(report) {
|
|
2648
|
+
return [
|
|
2649
|
+
["Sessions in collected app buckets", number(report.activity.sessions)],
|
|
2650
|
+
["Violating sessions", number(report.activity.violatingSessions)],
|
|
2651
|
+
[
|
|
2652
|
+
"Violation rate",
|
|
2653
|
+
report.activity.violationRate === null ? "Unknown" : `${number(report.activity.violationRate)}%`
|
|
2654
|
+
],
|
|
2655
|
+
[
|
|
2656
|
+
"Complete evidence sources",
|
|
2657
|
+
`${report.sources.filter((source2) => source2.status === "complete").length} / ${report.sources.length}`
|
|
2658
|
+
]
|
|
2659
|
+
];
|
|
2660
|
+
}
|
|
2661
|
+
function renderRuntimeReportMarkdown(report) {
|
|
2662
|
+
const lines = [
|
|
2663
|
+
`# ${md(report.title)}`,
|
|
2664
|
+
"",
|
|
2665
|
+
`**${md(report.product)}**`,
|
|
2666
|
+
"",
|
|
2667
|
+
`Assessment: **${HEALTH[report.health]}**`,
|
|
2668
|
+
"",
|
|
2669
|
+
`Window: ${md(report.window.start)} \u2192 ${md(report.window.end)}`,
|
|
2670
|
+
"",
|
|
2671
|
+
`${md(report.window.description)}. Collection: ${md(report.collectionStartedAt)} \u2192 ${md(report.generatedAt)}.`,
|
|
2672
|
+
"",
|
|
2673
|
+
"## At a glance",
|
|
2674
|
+
"",
|
|
2675
|
+
...metrics(report).map(([key, value]) => `- ${key}: **${value}**`),
|
|
2676
|
+
"",
|
|
2677
|
+
"## What needs attention",
|
|
2678
|
+
""
|
|
2679
|
+
];
|
|
2680
|
+
if (report.findings.length === 0)
|
|
2681
|
+
lines.push(
|
|
2682
|
+
"No findings in the available evidence. This does not establish an absence of risk.",
|
|
2683
|
+
""
|
|
2684
|
+
);
|
|
2685
|
+
for (const finding of report.findings)
|
|
2686
|
+
lines.push(
|
|
2687
|
+
`### ${finding.priority.toUpperCase()}: ${md(finding.title)}`,
|
|
2688
|
+
"",
|
|
2689
|
+
`Evidence: ${md(finding.evidence)}`,
|
|
2690
|
+
"",
|
|
2691
|
+
`Next step: ${md(finding.recommendation)}`,
|
|
2692
|
+
"",
|
|
2693
|
+
`Source: ${md(finding.source)}`,
|
|
2694
|
+
""
|
|
2695
|
+
);
|
|
2696
|
+
for (const table of tables(report)) {
|
|
2697
|
+
lines.push(`## ${table.title}`, "", md(table.note), "");
|
|
2698
|
+
if (table.rows.length === 0)
|
|
2699
|
+
lines.push(
|
|
2700
|
+
"No rows available. Consult the source status before interpreting this as an empty result.",
|
|
2701
|
+
""
|
|
2702
|
+
);
|
|
2703
|
+
else
|
|
2704
|
+
lines.push(
|
|
2705
|
+
`| ${table.headers.join(" | ")} |`,
|
|
2706
|
+
`| ${table.headers.map(() => "---").join(" | ")} |`,
|
|
2707
|
+
...table.rows.map((row) => `| ${row.map(md).join(" | ")} |`),
|
|
2708
|
+
""
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
lines.push(
|
|
2712
|
+
"## Scope, privacy, and limitations",
|
|
2713
|
+
"",
|
|
2714
|
+
...report.limitations.map((note) => `- ${md(note)}`),
|
|
2715
|
+
"",
|
|
2716
|
+
`Generated by Prisma AIRS CLI \xB7 Report schema ${report.schemaVersion}`,
|
|
2717
|
+
""
|
|
2718
|
+
);
|
|
2719
|
+
return lines.join("\n");
|
|
2720
|
+
}
|
|
2721
|
+
var CSS = `
|
|
2722
|
+
#evidence td:nth-child(3){white-space:nowrap}
|
|
2723
|
+
: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
|
+
*{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}
|
|
2725
|
+
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}
|
|
2726
|
+
@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
|
+
@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
|
+
`;
|
|
2729
|
+
var JS = `
|
|
2730
|
+
'use strict';
|
|
2731
|
+
document.documentElement.classList.add('js-ready');
|
|
2732
|
+
document.getElementById('print-report').addEventListener('click',function(){window.print();});
|
|
2733
|
+
var filter=document.getElementById('finding-filter');
|
|
2734
|
+
filter.addEventListener('change',function(){
|
|
2735
|
+
var visible=0;
|
|
2736
|
+
document.querySelectorAll('.finding').forEach(function(row){row.hidden=filter.value!=='all'&&row.dataset.priority!==filter.value;if(!row.hidden)visible++;});
|
|
2737
|
+
document.getElementById('finding-count').textContent=visible+' findings shown';
|
|
2738
|
+
});
|
|
2739
|
+
var search=document.getElementById('app-search');
|
|
2740
|
+
search.addEventListener('input',function(){
|
|
2741
|
+
var query=search.value.toLowerCase().trim(),visible=0;
|
|
2742
|
+
document.querySelectorAll('#applications tbody tr').forEach(function(row){row.hidden=!row.textContent.toLowerCase().includes(query);if(!row.hidden)visible++;});
|
|
2743
|
+
document.getElementById('app-count').textContent=visible+' application buckets shown';
|
|
2744
|
+
});
|
|
2745
|
+
`;
|
|
2746
|
+
function renderRuntimeReportHtml(report) {
|
|
2747
|
+
const hash = (text) => createHash("sha256").update(text).digest("base64");
|
|
2748
|
+
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'`;
|
|
2749
|
+
const findingCards = report.findings.map(
|
|
2750
|
+
(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>`
|
|
2751
|
+
).join("");
|
|
2752
|
+
const sections = tables(report).map(
|
|
2753
|
+
(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>`
|
|
2754
|
+
).join("");
|
|
2755
|
+
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="${html(policy)}"><meta name="referrer" content="no-referrer"><title>${html(report.title)} \u2014 Prisma AIRS</title><style>${CSS}</style></head>
|
|
2757
|
+
<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>
|
|
2758
|
+
<main id="main"><nav aria-label="Report sections"><a href="#findings">Attention</a><a href="#applications">Activity</a><a href="#profiles">Profiles</a><a href="#inventory">Inventory</a><a href="#evidence">Evidence</a><a href="#scope">Scope</a></nav>
|
|
2759
|
+
<div class="cards">${metrics(report).map(([name, value]) => `<div class="card"><span>${name}</span><strong>${value}</strong></div>`).join("")}</div>
|
|
2760
|
+
<section id="findings"><h2>What needs attention</h2><p class="note">Observed violations and configuration review items, not a numerical health score. Collection gaps remain visible in the evidence table.</p><div class="toolbar js-only"><label for="finding-filter">Priority</label><select id="finding-filter"><option value="all">All findings</option><option value="attention">Attention</option><option value="review">Review</option><option value="info">Information</option></select><span id="finding-count" role="status"></span><button id="print-report" type="button">Print / save PDF</button></div>${findingCards || "<p>No findings in the available evidence. This does not establish an absence of risk.</p>"}</section>
|
|
2761
|
+
${sections}<section id="scope"><h2>Scope, privacy, and limitations</h2><ul>${report.limitations.map((note) => `<li>${html(note)}</li>`).join("")}</ul></section><footer>Generated by Prisma AIRS CLI \xB7 Report schema ${report.schemaVersion} \xB7 Self-contained HTML; no network access required. All evidence remains readable with JavaScript disabled.</footer></main><script>${JS}</script></body></html>
|
|
2762
|
+
`;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// src/reports/runtime.ts
|
|
2766
|
+
var PAGE_SIZE = 100;
|
|
2767
|
+
var DAY_MS = 864e5;
|
|
2768
|
+
var ReportDataError = class extends Error {
|
|
2769
|
+
};
|
|
2770
|
+
function safeFailure(error) {
|
|
2771
|
+
if (error instanceof ReportDataError) return error.message;
|
|
2772
|
+
const code = error?.statusCode;
|
|
2773
|
+
if (typeof code === "number" && Number.isInteger(code) && code >= 400 && code <= 599) {
|
|
2774
|
+
if (code === 401 || code === 403)
|
|
2775
|
+
return `HTTP ${code}: check Management API credentials and grants.`;
|
|
2776
|
+
if (code === 429) return "HTTP 429: rate limited; retry the report later.";
|
|
2777
|
+
return `HTTP ${code}: source request failed; verify service availability and API compatibility.`;
|
|
2778
|
+
}
|
|
2779
|
+
return "Source request or response validation failed; check connectivity and SDK compatibility.";
|
|
2780
|
+
}
|
|
2781
|
+
function label(value) {
|
|
2782
|
+
return (value?.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim() || "Unknown").slice(0, 240);
|
|
2783
|
+
}
|
|
2784
|
+
function count(value) {
|
|
2785
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
2786
|
+
}
|
|
2787
|
+
async function collect(source2, maxPages, read, identity) {
|
|
2788
|
+
const records = /* @__PURE__ */ new Map();
|
|
2789
|
+
let offset = 0;
|
|
2790
|
+
try {
|
|
2791
|
+
for (let page = 0; page < maxPages; page++) {
|
|
2792
|
+
const result = await read(offset);
|
|
2793
|
+
source2.pages++;
|
|
2794
|
+
if (!Array.isArray(result.items))
|
|
2795
|
+
throw new ReportDataError(
|
|
2796
|
+
"The API returned no record array. An empty object or absent collection does not establish zero activity."
|
|
2797
|
+
);
|
|
2798
|
+
const before = records.size;
|
|
2799
|
+
for (const item of result.items) records.set(identity(item), item);
|
|
2800
|
+
source2.records = records.size;
|
|
2801
|
+
if (result.note) source2.notes.push(result.note);
|
|
2802
|
+
if (records.size - before !== result.items.length) {
|
|
2803
|
+
source2.notes.push("Overlapping or repeated records detected; duplicates excluded.");
|
|
2804
|
+
source2.status = "partial";
|
|
2805
|
+
}
|
|
2806
|
+
if (result.next === void 0) {
|
|
2807
|
+
if (source2.status !== "partial") source2.status = "complete";
|
|
2808
|
+
return [...records.values()];
|
|
2809
|
+
}
|
|
2810
|
+
if (!Number.isSafeInteger(result.next) || result.next <= offset || records.size === before) {
|
|
2811
|
+
source2.notes.push("Pagination did not advance; collection stopped safely.");
|
|
2812
|
+
source2.status = "partial";
|
|
2813
|
+
return [...records.values()];
|
|
2814
|
+
}
|
|
2815
|
+
offset = result.next;
|
|
2816
|
+
}
|
|
2817
|
+
source2.notes.push(`Reached the ${maxPages}-page budget; this is not a complete inventory.`);
|
|
2818
|
+
source2.status = "partial";
|
|
2819
|
+
} catch (error) {
|
|
2820
|
+
source2.status = source2.pages > 0 && records.size > 0 ? "partial" : "unavailable";
|
|
2821
|
+
source2.notes.push(safeFailure(error));
|
|
2822
|
+
}
|
|
2823
|
+
return [...records.values()];
|
|
2824
|
+
}
|
|
2825
|
+
function source(name, method, window) {
|
|
2826
|
+
return { name, method, window, status: "unavailable", records: 0, pages: 0, notes: [] };
|
|
2827
|
+
}
|
|
2828
|
+
function inventoryNext(next, size, offset) {
|
|
2829
|
+
return next === 0 ? void 0 : next ?? (size === PAGE_SIZE ? offset + size : void 0);
|
|
2830
|
+
}
|
|
2831
|
+
function total(values) {
|
|
2832
|
+
if (values.some((value) => value === null)) return null;
|
|
2833
|
+
const result = values.reduce((sum, value) => sum + (value ?? 0), 0);
|
|
2834
|
+
return count(result);
|
|
2835
|
+
}
|
|
2836
|
+
function distribution(rows) {
|
|
2837
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2838
|
+
for (const row of rows) {
|
|
2839
|
+
const name = label(row.violation_status).toLowerCase();
|
|
2840
|
+
groups.set(name, (groups.get(name) ?? 0) + 1);
|
|
2841
|
+
}
|
|
2842
|
+
return [...groups].map(([name, n]) => ({ name, count: n })).sort((a, b) => b.count - a.count);
|
|
2843
|
+
}
|
|
2844
|
+
function severity(value) {
|
|
2845
|
+
return {
|
|
2846
|
+
critical: count(value.critical),
|
|
2847
|
+
high: count(value.high),
|
|
2848
|
+
medium: count(value.medium),
|
|
2849
|
+
low: count(value.low),
|
|
2850
|
+
total: count(value.total)
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
async function collectRuntimeDailyReport(client, options = {}) {
|
|
2854
|
+
const maxPages = options.maxPages ?? 40;
|
|
2855
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 100) {
|
|
2856
|
+
throw new Error("maxPages must be an integer from 1 to 100");
|
|
2857
|
+
}
|
|
2858
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2859
|
+
const started = now();
|
|
2860
|
+
const end = started.toISOString();
|
|
2861
|
+
const start = new Date(started.getTime() - DAY_MS).toISOString();
|
|
2862
|
+
const activitySource = source(
|
|
2863
|
+
"Daily application activity",
|
|
2864
|
+
"dashboard.applicationsOverview",
|
|
2865
|
+
"Rolling 1 day"
|
|
2866
|
+
);
|
|
2867
|
+
const profileSource = source(
|
|
2868
|
+
"Security profiles",
|
|
2869
|
+
"profiles.list (latest=true)",
|
|
2870
|
+
"Current configuration"
|
|
2871
|
+
);
|
|
2872
|
+
const appSource = source("Registered applications", "customerApps.list", "Current configuration");
|
|
2873
|
+
const sessionSource = source(
|
|
2874
|
+
"Daily session inventory",
|
|
2875
|
+
"dashboard.sessionsOverview",
|
|
2876
|
+
"Rolling 1 day"
|
|
2877
|
+
);
|
|
2878
|
+
const chartSource = source("Daily session chart", "dashboard.sessionsChart", "Rolling 1 day");
|
|
2879
|
+
const rankingSource = source(
|
|
2880
|
+
"Top application violations",
|
|
2881
|
+
"dashboard.topApplicationsViolations",
|
|
2882
|
+
"Rolling 1 day; server-ranked subset"
|
|
2883
|
+
);
|
|
2884
|
+
const trendSource = source(
|
|
2885
|
+
"Daily violation trend",
|
|
2886
|
+
"dashboard.applicationsViolationsTrend",
|
|
2887
|
+
"Rolling 1 day"
|
|
2888
|
+
);
|
|
2889
|
+
const sources = [
|
|
2890
|
+
activitySource,
|
|
2891
|
+
profileSource,
|
|
2892
|
+
appSource,
|
|
2893
|
+
sessionSource,
|
|
2894
|
+
chartSource,
|
|
2895
|
+
rankingSource,
|
|
2896
|
+
trendSource
|
|
2897
|
+
];
|
|
2898
|
+
const daily = { timeInterval: 1, timeUnit: "day" };
|
|
2899
|
+
let expectedSessionTotal;
|
|
2900
|
+
let expectedAppTotal;
|
|
2901
|
+
const [applications, profileRows, registered, rawSessions, chart, rankings, trend] = await Promise.all([
|
|
2902
|
+
collect(
|
|
2903
|
+
activitySource,
|
|
2904
|
+
maxPages,
|
|
2905
|
+
async (offset) => {
|
|
2906
|
+
const page = await client.dashboard.applicationsOverview({
|
|
2907
|
+
timeInterval: 1,
|
|
2908
|
+
timeUnit: "day",
|
|
2909
|
+
limit: PAGE_SIZE,
|
|
2910
|
+
offset
|
|
2911
|
+
});
|
|
2912
|
+
const size = page.items?.length ?? 0;
|
|
2913
|
+
const totalItems = count(page.pagination?.total_items);
|
|
2914
|
+
if (totalItems !== null) {
|
|
2915
|
+
if (expectedAppTotal !== void 0 && totalItems !== expectedAppTotal)
|
|
2916
|
+
throw new ReportDataError(
|
|
2917
|
+
"Application total changed during pagination; retry for a stable inventory."
|
|
2918
|
+
);
|
|
2919
|
+
expectedAppTotal = totalItems;
|
|
2920
|
+
if (offset + size > totalItems)
|
|
2921
|
+
throw new ReportDataError("Application records exceed the advertised total.");
|
|
2922
|
+
}
|
|
2923
|
+
if (totalItems !== null && totalItems > offset + size && size === 0) {
|
|
2924
|
+
throw new ReportDataError(
|
|
2925
|
+
"The API returned an empty page before its advertised total."
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
return {
|
|
2929
|
+
items: page.items,
|
|
2930
|
+
next: totalItems !== null ? offset + size < totalItems ? offset + size : void 0 : size === PAGE_SIZE ? offset + size : void 0
|
|
2931
|
+
};
|
|
2932
|
+
},
|
|
2933
|
+
(item) => JSON.stringify([item.id, item.name])
|
|
2934
|
+
),
|
|
2935
|
+
collect(
|
|
2936
|
+
profileSource,
|
|
2937
|
+
maxPages,
|
|
2938
|
+
async (offset) => {
|
|
2939
|
+
const page = await client.profiles.list({ latest: true, limit: PAGE_SIZE, offset });
|
|
2940
|
+
return {
|
|
2941
|
+
items: page.ai_profiles,
|
|
2942
|
+
next: inventoryNext(page.next_offset, page.ai_profiles.length, offset)
|
|
2943
|
+
};
|
|
2944
|
+
},
|
|
2945
|
+
(item) => JSON.stringify([item.profile_name, item.revision])
|
|
2946
|
+
),
|
|
2947
|
+
collect(
|
|
2948
|
+
appSource,
|
|
2949
|
+
maxPages,
|
|
2950
|
+
async (offset) => {
|
|
2951
|
+
const page = await client.customerApps.list({ limit: PAGE_SIZE, offset });
|
|
2952
|
+
return {
|
|
2953
|
+
items: page.customer_apps,
|
|
2954
|
+
next: inventoryNext(page.next_offset, page.customer_apps?.length ?? 0, offset)
|
|
2955
|
+
};
|
|
2956
|
+
},
|
|
2957
|
+
(item) => item.customer_appId
|
|
2958
|
+
),
|
|
2959
|
+
collect(
|
|
2960
|
+
sessionSource,
|
|
2961
|
+
maxPages,
|
|
2962
|
+
async (offset) => {
|
|
2963
|
+
const page = await client.dashboard.sessionsOverview({ ...daily, limit: 25, offset });
|
|
2964
|
+
const size = page.items?.length ?? 0;
|
|
2965
|
+
const totalItems = count(page.pagination?.total_items);
|
|
2966
|
+
if (totalItems === null || page.pagination?.skip !== offset || page.pagination?.limit !== 25)
|
|
2967
|
+
throw new ReportDataError("Missing or mismatched session pagination metadata.");
|
|
2968
|
+
if (expectedSessionTotal !== void 0 && expectedSessionTotal !== totalItems)
|
|
2969
|
+
throw new ReportDataError(
|
|
2970
|
+
"Session total changed during pagination; retry for a stable inventory."
|
|
2971
|
+
);
|
|
2972
|
+
expectedSessionTotal = totalItems;
|
|
2973
|
+
if (offset + size > totalItems || size > 25 || size === 0 && offset < totalItems)
|
|
2974
|
+
throw new ReportDataError("Session page does not agree with its advertised total.");
|
|
2975
|
+
return {
|
|
2976
|
+
items: page.items,
|
|
2977
|
+
next: offset + size < totalItems ? offset + size : void 0
|
|
2978
|
+
};
|
|
2979
|
+
},
|
|
2980
|
+
(item) => JSON.stringify([item.application_id, item.application_name, item.session_id])
|
|
2981
|
+
),
|
|
2982
|
+
collect(
|
|
2983
|
+
chartSource,
|
|
2984
|
+
1,
|
|
2985
|
+
async () => ({ items: (await client.dashboard.sessionsChart(daily)).buckets }),
|
|
2986
|
+
(item) => String(item.bucket_number)
|
|
2987
|
+
),
|
|
2988
|
+
collect(
|
|
2989
|
+
rankingSource,
|
|
2990
|
+
1,
|
|
2991
|
+
async () => ({
|
|
2992
|
+
items: (await client.dashboard.topApplicationsViolations(daily)).applications
|
|
2993
|
+
}),
|
|
2994
|
+
(item) => JSON.stringify([item.id, item.name])
|
|
2995
|
+
),
|
|
2996
|
+
collect(
|
|
2997
|
+
trendSource,
|
|
2998
|
+
1,
|
|
2999
|
+
async () => ({
|
|
3000
|
+
items: (await client.dashboard.applicationsViolationsTrend(daily)).violations
|
|
3001
|
+
}),
|
|
3002
|
+
(item) => String(item.bucket_number)
|
|
3003
|
+
)
|
|
3004
|
+
]);
|
|
3005
|
+
const findings = [];
|
|
3006
|
+
const add = (priority, title, evidence, recommendation, evidenceSource) => {
|
|
3007
|
+
findings.push({ priority, title, evidence, recommendation, source: evidenceSource });
|
|
3008
|
+
};
|
|
3009
|
+
for (const s of sources) {
|
|
3010
|
+
if (s.status !== "complete")
|
|
3011
|
+
add(
|
|
3012
|
+
"review",
|
|
3013
|
+
`${s.name}: ${s.status}`,
|
|
3014
|
+
s.notes.join(" "),
|
|
3015
|
+
"Restore access or retry collection; do not treat missing data as zero. Increase --max-pages if the page budget was reached.",
|
|
3016
|
+
s.name
|
|
3017
|
+
);
|
|
3018
|
+
}
|
|
3019
|
+
const appIds = new Set(registered.map((app) => app.customer_appId));
|
|
3020
|
+
const reportApps = applications.map((app) => ({
|
|
3021
|
+
name: label(app.name),
|
|
3022
|
+
sessions: count(app.sessions_total),
|
|
3023
|
+
violatingSessions: count(app.sessions_violated),
|
|
3024
|
+
registered: app.id && appIds.has(app.id) ? true : appSource.status === "complete" && app.id ? false : null
|
|
3025
|
+
})).sort(
|
|
3026
|
+
(a, b) => (b.violatingSessions ?? -1) - (a.violatingSessions ?? -1) || a.name.localeCompare(b.name)
|
|
3027
|
+
);
|
|
3028
|
+
const sessions = activitySource.status === "unavailable" ? null : total(reportApps.map((app) => app.sessions));
|
|
3029
|
+
const violatingSessions = activitySource.status === "unavailable" ? null : total(reportApps.map((app) => app.violatingSessions));
|
|
3030
|
+
const inconsistent = reportApps.some(
|
|
3031
|
+
(app) => app.sessions !== null && app.violatingSessions !== null && app.violatingSessions > app.sessions
|
|
3032
|
+
);
|
|
3033
|
+
if (inconsistent || reportApps.some((app) => app.sessions === null || app.violatingSessions === null)) {
|
|
3034
|
+
add(
|
|
3035
|
+
"review",
|
|
3036
|
+
"Activity counters need verification",
|
|
3037
|
+
"Some application counters are absent, invalid, or report more violating sessions than total sessions.",
|
|
3038
|
+
"Compare these buckets with the SCM dashboard. A violation rate is withheld when counters are inconsistent.",
|
|
3039
|
+
activitySource.name
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
if ((violatingSessions ?? 0) > 0)
|
|
3043
|
+
add(
|
|
3044
|
+
"attention",
|
|
3045
|
+
"Violating sessions observed",
|
|
3046
|
+
`${violatingSessions} violating sessions across ${reportApps.filter((app) => (app.violatingSessions ?? 0) > 0).length} application buckets in the collected daily data.`,
|
|
3047
|
+
"Review the highest-volume applications in SCM, confirm intended enforcement, and investigate unexpected activity. Violations are not proof of a successful attack.",
|
|
3048
|
+
activitySource.name
|
|
3049
|
+
);
|
|
3050
|
+
if (sessions === 0 && activitySource.status === "complete")
|
|
3051
|
+
add(
|
|
3052
|
+
"review",
|
|
3053
|
+
"No sessions reported in the daily window",
|
|
3054
|
+
"The complete application overview reports zero sessions.",
|
|
3055
|
+
"If traffic was expected, check the integration, application metadata, and telemetry ingestion. No activity is not proof of health.",
|
|
3056
|
+
activitySource.name
|
|
3057
|
+
);
|
|
3058
|
+
const latestProfiles = /* @__PURE__ */ new Map();
|
|
3059
|
+
for (const profile of profileRows) {
|
|
3060
|
+
const old = latestProfiles.get(profile.profile_name);
|
|
3061
|
+
if (!old || (profile.revision ?? -1) > (old.revision ?? -1))
|
|
3062
|
+
latestProfiles.set(profile.profile_name, profile);
|
|
3063
|
+
}
|
|
3064
|
+
const profiles = [...latestProfiles.values()].map((profile) => {
|
|
3065
|
+
const configs = (profile.policy?.["ai-security-profiles"] ?? []).flatMap(
|
|
3066
|
+
(p) => p["model-configuration"] ? [p["model-configuration"]] : []
|
|
3067
|
+
);
|
|
3068
|
+
const timeoutActions = [
|
|
3069
|
+
...new Set(configs.map((c) => label(c.latency?.["inline-timeout-action"])))
|
|
3070
|
+
];
|
|
3071
|
+
const name = label(profile.profile_name);
|
|
3072
|
+
if (profile.active === false)
|
|
3073
|
+
add(
|
|
3074
|
+
"review",
|
|
3075
|
+
`Inactive profile: ${name}`,
|
|
3076
|
+
"The latest returned revision is explicitly inactive.",
|
|
3077
|
+
"Confirm this is intentional before assigning the profile to an integration.",
|
|
3078
|
+
profileSource.name
|
|
3079
|
+
);
|
|
3080
|
+
if (profile.active === true && timeoutActions.some((action) => action.toLowerCase() === "allow"))
|
|
3081
|
+
add(
|
|
3082
|
+
"review",
|
|
3083
|
+
`Timeout allows traffic: ${name}`,
|
|
3084
|
+
"An active profile explicitly sets inline-timeout-action to allow.",
|
|
3085
|
+
"Review the availability-versus-enforcement tradeoff with the application owner; this is not evidence of a timeout or bypass.",
|
|
3086
|
+
profileSource.name
|
|
3087
|
+
);
|
|
3088
|
+
const masking = configs.map((c) => c["mask-data-in-storage"]);
|
|
3089
|
+
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";
|
|
3090
|
+
if (profile.active === true && masking.includes(false))
|
|
3091
|
+
add(
|
|
3092
|
+
"review",
|
|
3093
|
+
`Storage masking disabled: ${name}`,
|
|
3094
|
+
"An active profile explicitly disables mask-data-in-storage in a model configuration.",
|
|
3095
|
+
"Confirm retention and privacy requirements. This flag alone does not establish that sensitive data was stored.",
|
|
3096
|
+
profileSource.name
|
|
3097
|
+
);
|
|
3098
|
+
return {
|
|
3099
|
+
name,
|
|
3100
|
+
revision: count(profile.revision),
|
|
3101
|
+
active: profile.active ?? null,
|
|
3102
|
+
modified: profile.last_modified_ts && Number.isFinite(Date.parse(profile.last_modified_ts)) ? new Date(profile.last_modified_ts).toISOString() : null,
|
|
3103
|
+
timeoutActions,
|
|
3104
|
+
storageMasking
|
|
3105
|
+
};
|
|
3106
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
3107
|
+
if (profiles.length === 0 && profileSource.status === "complete")
|
|
3108
|
+
add(
|
|
3109
|
+
"attention",
|
|
3110
|
+
"No security profiles returned",
|
|
3111
|
+
"The current profile inventory is empty.",
|
|
3112
|
+
"Verify tenant scope and configure a security profile before routing application scans.",
|
|
3113
|
+
profileSource.name
|
|
3114
|
+
);
|
|
3115
|
+
let missingTimestamps = 0;
|
|
3116
|
+
let outsideWindow = 0;
|
|
3117
|
+
const sessionRows = rawSessions.filter((row) => {
|
|
3118
|
+
const timestamp = Date.parse(row.last_session_activity ?? "");
|
|
3119
|
+
if (!Number.isFinite(timestamp)) {
|
|
3120
|
+
missingTimestamps++;
|
|
3121
|
+
return false;
|
|
3122
|
+
}
|
|
3123
|
+
if (timestamp < Date.parse(start) || timestamp >= Date.parse(end)) {
|
|
3124
|
+
outsideWindow++;
|
|
3125
|
+
return false;
|
|
3126
|
+
}
|
|
3127
|
+
return true;
|
|
3128
|
+
});
|
|
3129
|
+
if (missingTimestamps || outsideWindow)
|
|
3130
|
+
add(
|
|
3131
|
+
"review",
|
|
3132
|
+
"Some session records excluded from daily analysis",
|
|
3133
|
+
`${missingTimestamps} records lack a valid last-activity timestamp; ${outsideWindow} fall outside the anchored UTC window.`,
|
|
3134
|
+
"Verify ingestion timestamps and collection-window drift before using session counts as daily totals.",
|
|
3135
|
+
sessionSource.name
|
|
3136
|
+
);
|
|
3137
|
+
const failedSessions = sessionRows.filter(
|
|
3138
|
+
(row) => ["failed", "error", "timeout"].includes(row.violation_status.trim().toLowerCase())
|
|
3139
|
+
).length;
|
|
3140
|
+
if (failedSessions)
|
|
3141
|
+
add(
|
|
3142
|
+
"attention",
|
|
3143
|
+
"Session processing failures observed",
|
|
3144
|
+
`${failedSessions} timestamp-eligible collected sessions explicitly report failed, error, or timeout status.`,
|
|
3145
|
+
"Review these sessions in SCM and check integration error handling. This is not a tenant-wide failure rate.",
|
|
3146
|
+
sessionSource.name
|
|
3147
|
+
);
|
|
3148
|
+
const chartSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) => count(bucket.total)));
|
|
3149
|
+
const chartViolatingSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) => count(bucket.violating)));
|
|
3150
|
+
if (chartSessions !== null && sessionSource.status === "complete" && chartSessions !== rawSessions.length)
|
|
3151
|
+
add(
|
|
3152
|
+
"review",
|
|
3153
|
+
"Session sources disagree",
|
|
3154
|
+
`The chart reports ${chartSessions} sessions; the independently paginated inventory returned ${rawSessions.length}.`,
|
|
3155
|
+
"Preserve both measurements and verify in SCM. Separate rolling queries are not a consistent snapshot; the discrepancy has no verified cause.",
|
|
3156
|
+
chartSource.name
|
|
3157
|
+
);
|
|
3158
|
+
const urgentViolations = total(
|
|
3159
|
+
trend.map(
|
|
3160
|
+
(bucket) => total([count(bucket.violation_breakdown.critical), count(bucket.violation_breakdown.high)])
|
|
3161
|
+
)
|
|
3162
|
+
);
|
|
3163
|
+
if ((urgentViolations ?? 0) > 0)
|
|
3164
|
+
add(
|
|
3165
|
+
"attention",
|
|
3166
|
+
"High or critical detector violations observed",
|
|
3167
|
+
`${urgentViolations} high/critical detector violations in the returned daily trend. These are not distinct sessions.`,
|
|
3168
|
+
"Investigate the affected applications and confirm enforcement decisions before assigning incident severity.",
|
|
3169
|
+
trendSource.name
|
|
3170
|
+
);
|
|
3171
|
+
const priorities = { attention: 0, review: 1, info: 2 };
|
|
3172
|
+
findings.sort((left, right) => priorities[left.priority] - priorities[right.priority]);
|
|
3173
|
+
return {
|
|
3174
|
+
schemaVersion: 2,
|
|
3175
|
+
product: "Prisma AIRS AI Runtime Security",
|
|
3176
|
+
title: label(options.title ?? "Daily environment report"),
|
|
3177
|
+
generatedAt: now().toISOString(),
|
|
3178
|
+
collectionStartedAt: end,
|
|
3179
|
+
window: {
|
|
3180
|
+
start,
|
|
3181
|
+
end,
|
|
3182
|
+
description: "Rolling last 24 hours, UTC (approximate for server-relative application queries)"
|
|
3183
|
+
},
|
|
3184
|
+
health: sources.every((s) => s.status === "unavailable") ? "unknown" : findings.some((f) => f.priority === "attention") ? "attention" : findings.some((f) => f.priority === "review") ? "review" : "no-findings",
|
|
3185
|
+
sources,
|
|
3186
|
+
findings,
|
|
3187
|
+
activity: {
|
|
3188
|
+
sessions,
|
|
3189
|
+
violatingSessions,
|
|
3190
|
+
violationRate: !inconsistent && sessions !== null && sessions > 0 && violatingSessions !== null ? violatingSessions / sessions * 100 : null,
|
|
3191
|
+
applications: reportApps
|
|
3192
|
+
},
|
|
3193
|
+
profiles,
|
|
3194
|
+
registeredApps: registered.map((app) => ({
|
|
3195
|
+
name: label(app.app_name),
|
|
3196
|
+
environment: label(app.environment),
|
|
3197
|
+
cloud: label(app.cloud_provider),
|
|
3198
|
+
model: label(app.model_name),
|
|
3199
|
+
keyAssociations: app.api_keys_dp_info ? app.api_keys_dp_info.length : null
|
|
3200
|
+
})).sort((a, b) => a.name.localeCompare(b.name)),
|
|
3201
|
+
sessions: {
|
|
3202
|
+
entries: sessionSource.status === "unavailable" ? null : sessionRows.length,
|
|
3203
|
+
statuses: distribution(sessionRows),
|
|
3204
|
+
violatingSessions: sessionSource.status === "unavailable" ? null : sessionRows.filter((row) => row.violation_status === "violated").length,
|
|
3205
|
+
missingTimestamps,
|
|
3206
|
+
outsideWindow
|
|
3207
|
+
},
|
|
3208
|
+
dailyTelemetry: {
|
|
3209
|
+
chart: {
|
|
3210
|
+
sessions: chartSessions,
|
|
3211
|
+
violatingSessions: chartViolatingSessions,
|
|
3212
|
+
buckets: chart.map((bucket) => ({
|
|
3213
|
+
time: label(bucket.time),
|
|
3214
|
+
sessions: count(bucket.total),
|
|
3215
|
+
violatingSessions: count(bucket.violating),
|
|
3216
|
+
violations: severity(bucket.violation_breakdown)
|
|
3217
|
+
}))
|
|
3218
|
+
},
|
|
3219
|
+
topApplications: rankings.map((app) => ({
|
|
3220
|
+
name: label(app.name),
|
|
3221
|
+
violations: count(app.total_violations),
|
|
3222
|
+
detectors: app.policy_violations.map((detector) => ({
|
|
3223
|
+
name: label(detector.detection_type),
|
|
3224
|
+
count: count(detector.total)
|
|
3225
|
+
}))
|
|
3226
|
+
})),
|
|
3227
|
+
violationTrend: trend.map((bucket) => ({
|
|
3228
|
+
time: label(bucket.date),
|
|
3229
|
+
violations: severity(bucket.violation_breakdown)
|
|
3230
|
+
}))
|
|
3231
|
+
},
|
|
3232
|
+
limitations: [
|
|
3233
|
+
"This is a read-only operational review, not an uptime SLA, compliance attestation, or proof that attacks succeeded or were blocked. No health score is invented.",
|
|
3234
|
+
"Application activity uses the API\u2019s rolling one-day window; each paginated request evaluates its own server-relative window. Collection is not a transactionally consistent historical snapshot. Ingestion may lag.",
|
|
3235
|
+
"Application buckets, session inventory, chart sessions, and detector violations are different measurements. They are not added together. Session inventory summaries use timestamp-eligible entries only; absent data is unknown.",
|
|
3236
|
+
"Application buckets are keyed by registered application ID plus the literal scan metadata.app_name, which can differ from registered names. Do not sum session counts as unique users or unique tenant-wide sessions.",
|
|
3237
|
+
"Current configuration is not a configuration-change audit. Missing policy settings are unknown, not disabled. Inactive profiles can be intentional; current profile state may differ from the revision used by a historical scan.",
|
|
3238
|
+
"Daily charts, rankings and severity trends come from their own one-day endpoints. Rankings are a server-selected subset, not a complete inventory. Per-app token summaries and drill-downs require longer windows; no daily token usage or previous-day comparison is inferred.",
|
|
3239
|
+
"The legacy ScanLogsClient / scan-logs query path is broken and under refactor. This report uses the verified dashboard session APIs instead. It never automatically fetches transactions or stored scan content.",
|
|
3240
|
+
"Prompts, responses, user identities/IPs, API keys, auth codes, tenant IDs, and raw errors are omitted. Application/profile names and configuration metadata remain confidential; review before sharing."
|
|
3241
|
+
]
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
|
|
2484
3245
|
export {
|
|
2485
3246
|
SdkManagementService,
|
|
2486
3247
|
getOrCreateManagementClient,
|
|
@@ -2505,5 +3266,9 @@ export {
|
|
|
2505
3266
|
validateExamples,
|
|
2506
3267
|
validateTopic,
|
|
2507
3268
|
computeMetrics,
|
|
2508
|
-
computeCategoryBreakdown
|
|
3269
|
+
computeCategoryBreakdown,
|
|
3270
|
+
writeReportFile,
|
|
3271
|
+
renderRuntimeReportMarkdown,
|
|
3272
|
+
renderRuntimeReportHtml,
|
|
3273
|
+
collectRuntimeDailyReport
|
|
2509
3274
|
};
|