@shield-agent/kya 0.33.6 → 0.35.3
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/.cursor-plugin/plugin.json +2 -2
- package/.mcp.json +1 -1
- package/LIMITATIONS.md +2 -1
- package/README.md +39 -640
- package/amp/settings.example.json +1 -1
- package/assets/report-blocked.png +0 -0
- package/assets/report-certify.png +0 -0
- package/assets/report-overview.png +0 -0
- package/assets/screenshot-features.json +18 -0
- package/claude/claude_desktop_config.example.json +1 -1
- package/cline/cline_mcp_settings.example.json +1 -1
- package/copilot/mcp-config.example.json +1 -1
- package/dist/alerts/config.d.ts +22 -0
- package/dist/alerts/config.d.ts.map +1 -0
- package/dist/alerts/config.js +59 -0
- package/dist/alerts/config.js.map +1 -0
- package/dist/alerts/constants.d.ts +5 -0
- package/dist/alerts/constants.d.ts.map +1 -0
- package/dist/alerts/constants.js +5 -0
- package/dist/alerts/constants.js.map +1 -0
- package/dist/alerts/desktop.d.ts +19 -0
- package/dist/alerts/desktop.d.ts.map +1 -0
- package/dist/alerts/desktop.js +57 -0
- package/dist/alerts/desktop.js.map +1 -0
- package/dist/alerts/incident-alert.d.ts +42 -0
- package/dist/alerts/incident-alert.d.ts.map +1 -0
- package/dist/alerts/incident-alert.js +84 -0
- package/dist/alerts/incident-alert.js.map +1 -0
- package/dist/alerts/scan.d.ts +37 -0
- package/dist/alerts/scan.d.ts.map +1 -0
- package/dist/alerts/scan.js +188 -0
- package/dist/alerts/scan.js.map +1 -0
- package/dist/alerts/state.d.ts +55 -0
- package/dist/alerts/state.d.ts.map +1 -0
- package/dist/alerts/state.js +178 -0
- package/dist/alerts/state.js.map +1 -0
- package/dist/alerts/triage-input.d.ts +36 -0
- package/dist/alerts/triage-input.d.ts.map +1 -0
- package/dist/alerts/triage-input.js +159 -0
- package/dist/alerts/triage-input.js.map +1 -0
- package/dist/alerts/triage-model.d.ts +42 -0
- package/dist/alerts/triage-model.d.ts.map +1 -0
- package/dist/alerts/triage-model.js +126 -0
- package/dist/alerts/triage-model.js.map +1 -0
- package/dist/alerts/triage-prompt.d.ts +23 -0
- package/dist/alerts/triage-prompt.d.ts.map +1 -0
- package/dist/alerts/triage-prompt.js +53 -0
- package/dist/alerts/triage-prompt.js.map +1 -0
- package/dist/alerts/triage-schema.json +52 -0
- package/dist/alerts/triage-setup.d.ts +77 -0
- package/dist/alerts/triage-setup.d.ts.map +1 -0
- package/dist/alerts/triage-setup.js +290 -0
- package/dist/alerts/triage-setup.js.map +1 -0
- package/dist/alerts/triage-status.d.ts +26 -0
- package/dist/alerts/triage-status.d.ts.map +1 -0
- package/dist/alerts/triage-status.js +61 -0
- package/dist/alerts/triage-status.js.map +1 -0
- package/dist/alerts/triage-tokenizer.d.ts +29 -0
- package/dist/alerts/triage-tokenizer.d.ts.map +1 -0
- package/dist/alerts/triage-tokenizer.js +82 -0
- package/dist/alerts/triage-tokenizer.js.map +1 -0
- package/dist/card-png.d.ts +3 -0
- package/dist/card-png.d.ts.map +1 -0
- package/dist/card-png.js +54 -0
- package/dist/card-png.js.map +1 -0
- package/dist/card.d.ts +62 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/card.js +154 -0
- package/dist/card.js.map +1 -0
- package/dist/certify/evaluate.d.ts +11 -1
- package/dist/certify/evaluate.d.ts.map +1 -1
- package/dist/certify/evaluate.js +8 -1
- package/dist/certify/evaluate.js.map +1 -1
- package/dist/certify/render.d.ts.map +1 -1
- package/dist/certify/render.js +10 -1
- package/dist/certify/render.js.map +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +234 -49
- package/dist/cli.js.map +1 -1
- package/dist/commands/alerts.d.ts +16 -0
- package/dist/commands/alerts.d.ts.map +1 -0
- package/dist/commands/alerts.js +112 -0
- package/dist/commands/alerts.js.map +1 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +4 -3
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +12 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hook.d.ts.map +1 -1
- package/dist/commands/hook.js +94 -8
- package/dist/commands/hook.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/notify-flush.d.ts.map +1 -1
- package/dist/commands/notify-flush.js +3 -0
- package/dist/commands/notify-flush.js.map +1 -1
- package/dist/commands/receipt-serve.d.ts +1 -1
- package/dist/commands/receipt-serve.d.ts.map +1 -1
- package/dist/commands/receipt-serve.js +3 -2
- package/dist/commands/receipt-serve.js.map +1 -1
- package/dist/commands/receipt.d.ts +19 -0
- package/dist/commands/receipt.d.ts.map +1 -1
- package/dist/commands/receipt.js +21 -1
- package/dist/commands/receipt.js.map +1 -1
- package/dist/commands/serve-mcp.d.ts +2 -0
- package/dist/commands/serve-mcp.d.ts.map +1 -1
- package/dist/commands/serve-mcp.js +2 -0
- package/dist/commands/serve-mcp.js.map +1 -1
- package/dist/commands/start.d.ts +50 -2
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +233 -27
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/team.d.ts +19 -2
- package/dist/commands/team.d.ts.map +1 -1
- package/dist/commands/team.js +93 -13
- package/dist/commands/team.js.map +1 -1
- package/dist/commands/uninstall.d.ts +20 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +378 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/wire-hooks.d.ts +16 -2
- package/dist/commands/wire-hooks.d.ts.map +1 -1
- package/dist/commands/wire-hooks.js +86 -21
- package/dist/commands/wire-hooks.js.map +1 -1
- package/dist/cta.d.ts +3 -1
- package/dist/cta.d.ts.map +1 -1
- package/dist/cta.js +11 -6
- package/dist/cta.js.map +1 -1
- package/dist/fs-atomic.d.ts.map +1 -1
- package/dist/fs-atomic.js +9 -2
- package/dist/fs-atomic.js.map +1 -1
- package/dist/guard-demo.d.ts +23 -0
- package/dist/guard-demo.d.ts.map +1 -0
- package/dist/guard-demo.js +34 -0
- package/dist/guard-demo.js.map +1 -0
- package/dist/guard-entry.d.ts +2 -0
- package/dist/guard-entry.d.ts.map +1 -0
- package/dist/guard-entry.js +50 -0
- package/dist/guard-entry.js.map +1 -0
- package/dist/guard-lite.d.ts +27 -0
- package/dist/guard-lite.d.ts.map +1 -0
- package/dist/guard-lite.js +274 -0
- package/dist/guard-lite.js.map +1 -0
- package/dist/guard-pack.d.ts +7 -2
- package/dist/guard-pack.d.ts.map +1 -1
- package/dist/guard-pack.js +202 -4
- package/dist/guard-pack.js.map +1 -1
- package/dist/hook-health.d.ts +7 -0
- package/dist/hook-health.d.ts.map +1 -0
- package/dist/hook-health.js +105 -0
- package/dist/hook-health.js.map +1 -0
- package/dist/investigate/detectors.d.ts +6 -0
- package/dist/investigate/detectors.d.ts.map +1 -1
- package/dist/investigate/detectors.js +10 -0
- package/dist/investigate/detectors.js.map +1 -1
- package/dist/manifest.d.ts +106 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +454 -0
- package/dist/manifest.js.map +1 -0
- package/dist/mcp/http.d.ts +1 -0
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/http.js +1 -0
- package/dist/mcp/http.js.map +1 -1
- package/dist/mcp/protocol.d.ts +2 -0
- package/dist/mcp/protocol.d.ts.map +1 -1
- package/dist/mcp/protocol.js +5 -1
- package/dist/mcp/protocol.js.map +1 -1
- package/dist/mcp/stdio.d.ts +1 -0
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +1 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/never-list-input.d.ts +2 -0
- package/dist/never-list-input.d.ts.map +1 -1
- package/dist/never-list-input.js +30 -30
- package/dist/never-list-input.js.map +1 -1
- package/dist/notify/dispatch.d.ts +1 -1
- package/dist/notify/dispatch.d.ts.map +1 -1
- package/dist/notify/dispatch.js +4 -1
- package/dist/notify/dispatch.js.map +1 -1
- package/dist/notify/incident-templates.d.ts +23 -0
- package/dist/notify/incident-templates.d.ts.map +1 -0
- package/dist/notify/incident-templates.js +128 -0
- package/dist/notify/incident-templates.js.map +1 -0
- package/dist/notify/index.d.ts +12 -0
- package/dist/notify/index.d.ts.map +1 -1
- package/dist/notify/index.js +50 -3
- package/dist/notify/index.js.map +1 -1
- package/dist/notify/templates.d.ts +6 -0
- package/dist/notify/templates.d.ts.map +1 -1
- package/dist/notify/templates.js +12 -2
- package/dist/notify/templates.js.map +1 -1
- package/dist/offline-evaluate.d.ts.map +1 -1
- package/dist/offline-evaluate.js +2 -1
- package/dist/offline-evaluate.js.map +1 -1
- package/dist/parse-args.js +1 -1
- package/dist/parse-args.js.map +1 -1
- package/dist/receipt/changes.d.ts +2 -1
- package/dist/receipt/changes.d.ts.map +1 -1
- package/dist/receipt/changes.js +9 -0
- package/dist/receipt/changes.js.map +1 -1
- package/dist/receipt/dashboard.d.ts +14 -1
- package/dist/receipt/dashboard.d.ts.map +1 -1
- package/dist/receipt/dashboard.js +20 -2
- package/dist/receipt/dashboard.js.map +1 -1
- package/dist/receipt/live-server.d.ts +19 -0
- package/dist/receipt/live-server.d.ts.map +1 -1
- package/dist/receipt/live-server.js +119 -0
- package/dist/receipt/live-server.js.map +1 -1
- package/dist/receipt/receipt-css.d.ts.map +1 -1
- package/dist/receipt/receipt-css.js +45 -0
- package/dist/receipt/receipt-css.js.map +1 -1
- package/dist/receipt/render-receipt.d.ts +17 -0
- package/dist/receipt/render-receipt.d.ts.map +1 -1
- package/dist/receipt/render-receipt.js +479 -80
- package/dist/receipt/render-receipt.js.map +1 -1
- package/dist/receipt/search.d.ts.map +1 -1
- package/dist/receipt/search.js +2 -0
- package/dist/receipt/search.js.map +1 -1
- package/dist/receipt/semantic-setup.d.ts +4 -0
- package/dist/receipt/semantic-setup.d.ts.map +1 -1
- package/dist/receipt/semantic-setup.js +10 -1
- package/dist/receipt/semantic-setup.js.map +1 -1
- package/dist/receipt/share.d.ts +15 -0
- package/dist/receipt/share.d.ts.map +1 -1
- package/dist/receipt/share.js +34 -1
- package/dist/receipt/share.js.map +1 -1
- package/dist/telemetry.d.ts +17 -2
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +67 -7
- package/dist/telemetry.js.map +1 -1
- package/dist/trail.d.ts +45 -0
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +38 -0
- package/dist/trail.js.map +1 -1
- package/dist/write-journal.d.ts +39 -0
- package/dist/write-journal.d.ts.map +1 -0
- package/dist/write-journal.js +107 -0
- package/dist/write-journal.js.map +1 -0
- package/droid/README.md +1 -1
- package/droid/mcp.example.json +1 -1
- package/gemini/settings.example.json +1 -1
- package/grok/README.md +2 -2
- package/kilo/kilo.example.json +1 -1
- package/kimi/mcp.example.json +1 -1
- package/kiro/mcp.example.json +1 -1
- package/manifest.json +2 -2
- package/mastracode/mcp.example.json +1 -1
- package/mcp.json +1 -1
- package/openai/codex.config.example.toml +1 -1
- package/opencode/opencode.example.json +1 -1
- package/package.json +9 -3
- package/qwen/settings.example.json +1 -1
- package/server.json +3 -3
|
@@ -6,10 +6,13 @@ import { semanticInstallCommand, semanticRuntimeInstalled } from "./semantic-sta
|
|
|
6
6
|
import { assertNoSecrets, clip, stripEscapes } from "../dash/render.js";
|
|
7
7
|
import { clipMultiline, DIFF_MAX_TOTAL_CHARS } from "../diff-preview.js";
|
|
8
8
|
import { DOMAIN_LABELS, domainOrder } from "../certify/render.js";
|
|
9
|
-
import { productLabel, eventsSince, readTrail } from "../trail.js";
|
|
9
|
+
import { productLabel, eventsSince, readTrail, countOutcomes, outcomeOf, wouldHoldInStrict } from "../trail.js";
|
|
10
|
+
import { eventIdOf, isNearMiss } from "../card.js";
|
|
11
|
+
import { readCredentials } from "../login/credentials.js";
|
|
10
12
|
import { mcpServerLabel, parseMcpToolId } from "../mcp-servers.js";
|
|
11
13
|
import { loadInvestigateLastRun, } from "../investigate/last-run.js";
|
|
12
14
|
import { readNotifyLog, summarizeNotifyLog, } from "../notify/log.js";
|
|
15
|
+
import { readAlertsState } from "../alerts/state.js";
|
|
13
16
|
import { readOtelStats } from "../otel/stats.js";
|
|
14
17
|
import { loadCertifyCard, loadDiscoveryCard, loadIdentity, loadOrrCard, loadSandboxes, loadShowbackCard, loadWiredHosts, } from "./enrich.js";
|
|
15
18
|
import { SHOWBACK_DISCLAIMER, } from "../showback/cost-per-task.js";
|
|
@@ -18,6 +21,7 @@ import { computeDashboard } from "./dashboard.js";
|
|
|
18
21
|
import { buildChangesModel } from "./changes.js";
|
|
19
22
|
import { GATE_NOT_SETUP_QUICKSTART, loadGatePage, toolNameFromToolId, toolServerPrefix, } from "./gate-page.js";
|
|
20
23
|
import { receiptCss } from "./receipt-css.js";
|
|
24
|
+
import { DEMO_CALLS, PROMISE } from "../guard-demo.js";
|
|
21
25
|
import { applyChipFilters, rankEvents } from "./search.js";
|
|
22
26
|
import { emptyOpenshellCard, loadOpenshellCard } from "./openshell-card.js";
|
|
23
27
|
import { readIocResult } from "../ioc/result.js";
|
|
@@ -62,6 +66,11 @@ function mdFence(content) {
|
|
|
62
66
|
function mdInline(s) {
|
|
63
67
|
return stripEscapes(s).replace(/`/g, "'");
|
|
64
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Counts for the report headline. A row with an outcome is counted by what happened to the call (`outcomes`); only a
|
|
71
|
+
* row without one (written before outcomes existed) is counted by its stored verdict (allow, deny, require).
|
|
72
|
+
* `never` is a policy overlay and counts every row.
|
|
73
|
+
*/
|
|
65
74
|
function countVerdicts(events) {
|
|
66
75
|
let allow = 0;
|
|
67
76
|
let deny = 0;
|
|
@@ -71,6 +80,8 @@ function countVerdicts(events) {
|
|
|
71
80
|
const v = e.verdict.toUpperCase();
|
|
72
81
|
if (e.neverEvent || e.reasonCode === "NEVER_EVENT")
|
|
73
82
|
never++;
|
|
83
|
+
if (outcomeOf(e))
|
|
84
|
+
continue;
|
|
74
85
|
if (v === "ALLOW")
|
|
75
86
|
allow++;
|
|
76
87
|
else if (v === "DENY")
|
|
@@ -78,7 +89,55 @@ function countVerdicts(events) {
|
|
|
78
89
|
else if (v === "REQUIRE_APPROVE")
|
|
79
90
|
require++;
|
|
80
91
|
}
|
|
81
|
-
return { allow, deny, require, never };
|
|
92
|
+
return { allow, deny, require, never, outcomes: countOutcomes(events) };
|
|
93
|
+
}
|
|
94
|
+
const STRICT_NOTE = "would hold in strict mode";
|
|
95
|
+
/** One line under the headline: how to turn strict mode on. Shown only when a call ran that strict mode would hold (STRICT_NOTE is the row note). */
|
|
96
|
+
function strictHowTo(wouldHold, code) {
|
|
97
|
+
return `${wouldHold} ${wouldHold === 1 ? "call that ran" : "calls that ran"} would hold in strict mode. To turn strict mode on, add ${code("--strict")} to the ${code("kya hook")} command in your tool's hook settings.`;
|
|
98
|
+
}
|
|
99
|
+
/** Headline text for the Markdown report: "Ran 11, Blocked 5, Held 0". Error is listed only when it is not 0. */
|
|
100
|
+
function outcomeHeadline(o) {
|
|
101
|
+
return `Ran ${o.ran}, Blocked ${o.blocked}, Held ${o.held}${o.error > 0 ? `, Error ${o.error}` : ""}`;
|
|
102
|
+
}
|
|
103
|
+
const OUTCOME_WORD = { ran: "RAN", blocked: "BLOCKED", held: "HELD", error: "ERROR" };
|
|
104
|
+
/** A short note for a call that ran although policy said otherwise (REQUIRE_APPROVE, or DENY in shadow mode). */
|
|
105
|
+
function ranNote(e) {
|
|
106
|
+
if (wouldHoldInStrict(e))
|
|
107
|
+
return STRICT_NOTE;
|
|
108
|
+
if (outcomeOf(e) === "ran" && e.verdict.toUpperCase() === "DENY")
|
|
109
|
+
return "would block (shadow mode)";
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
/** Feed order: blocked rows (newest first) above everything else (newest first). Rows without an outcome sort as before. */
|
|
113
|
+
function compareFeed(a, b) {
|
|
114
|
+
const rank = (e) => (outcomeOf(e) === "blocked" ? 0 : 1);
|
|
115
|
+
return rank(a) - rank(b) || b.ts.localeCompare(a.ts);
|
|
116
|
+
}
|
|
117
|
+
/** Key for rows that may collapse. Only rows with an outcome collapse; a blocked row, or one with a change preview, never does. */
|
|
118
|
+
function collapseKey(e) {
|
|
119
|
+
const o = outcomeOf(e);
|
|
120
|
+
if (!o || o === "blocked" || e.diffPreview?.trim())
|
|
121
|
+
return undefined;
|
|
122
|
+
return JSON.stringify([e.toolId, o, e.verdict, e.reasonCode, e.summary ?? "", e.targetPath ?? "", e.argsHash ?? "", e.sessionId, e.product ?? "", e.project ?? "", e.mode]);
|
|
123
|
+
}
|
|
124
|
+
/** Merge identical consecutive rows ("shell echo, 10 times"). Input order is kept. */
|
|
125
|
+
export function collapseFeedRows(ordered) {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const e of ordered) {
|
|
128
|
+
const key = collapseKey(e);
|
|
129
|
+
const last = out[out.length - 1];
|
|
130
|
+
if (last && key !== undefined && last.key === key)
|
|
131
|
+
last.count++;
|
|
132
|
+
else
|
|
133
|
+
out.push({ event: e, count: 1, key });
|
|
134
|
+
}
|
|
135
|
+
return out.map(({ event, count }) => ({ event, count }));
|
|
136
|
+
}
|
|
137
|
+
/** "shell echo (+1 args), 3 times" or just "3 times" when the row has no summary. */
|
|
138
|
+
function repeatText(e, count) {
|
|
139
|
+
const base = e.summary?.trim();
|
|
140
|
+
return `${base ? `${base}, ` : ""}${count} times`;
|
|
82
141
|
}
|
|
83
142
|
function relativeTime(ts, nowMs) {
|
|
84
143
|
const t = Date.parse(ts);
|
|
@@ -141,6 +200,21 @@ function verdictWord(verdict) {
|
|
|
141
200
|
return "REVIEW";
|
|
142
201
|
return v;
|
|
143
202
|
}
|
|
203
|
+
/** Row tone: by outcome when the row has one (a call that ran is ok, even if policy said review), else by verdict. */
|
|
204
|
+
function rowTone(e) {
|
|
205
|
+
switch (outcomeOf(e)) {
|
|
206
|
+
case "ran": return "ok";
|
|
207
|
+
case "blocked": return "bad";
|
|
208
|
+
case "held": return "warn";
|
|
209
|
+
case "error": return "mute";
|
|
210
|
+
default: return tone(e.verdict);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Row label: RAN, BLOCKED, HELD or ERROR when the row has an outcome, else the verdict word as before. */
|
|
214
|
+
function rowWord(e) {
|
|
215
|
+
const o = outcomeOf(e);
|
|
216
|
+
return o ? OUTCOME_WORD[o] : verdictWord(e.verdict);
|
|
217
|
+
}
|
|
144
218
|
/** Pure rollup of trail events for the report's stat chips and sections. */
|
|
145
219
|
export function aggregateEvents(events) {
|
|
146
220
|
const modes = { observe: 0, hold: 0, offline: 0 };
|
|
@@ -181,8 +255,15 @@ export function aggregateEvents(events) {
|
|
|
181
255
|
s.events++;
|
|
182
256
|
if (e.neverEvent || e.reasonCode === "NEVER_EVENT")
|
|
183
257
|
s.never++;
|
|
258
|
+
const o = outcomeOf(e);
|
|
184
259
|
const v = e.verdict.toUpperCase();
|
|
185
|
-
if (
|
|
260
|
+
if (o) {
|
|
261
|
+
if (o === "blocked")
|
|
262
|
+
s.deny++;
|
|
263
|
+
else if (o === "held")
|
|
264
|
+
s.hold++;
|
|
265
|
+
}
|
|
266
|
+
else if (v === "DENY")
|
|
186
267
|
s.deny++;
|
|
187
268
|
else if (v === "REQUIRE_APPROVE")
|
|
188
269
|
s.hold++;
|
|
@@ -274,6 +355,7 @@ export function buildReceiptModel(sessionId, events, extras) {
|
|
|
274
355
|
liveToken: extras?.liveToken,
|
|
275
356
|
searchQuery: extras?.searchQuery,
|
|
276
357
|
identity: extras?.identity,
|
|
358
|
+
planeConfigured: extras?.planeConfigured,
|
|
277
359
|
sandboxes: extras?.sandboxes,
|
|
278
360
|
openshell: extras?.openshell,
|
|
279
361
|
wiredHosts: extras?.wiredHosts,
|
|
@@ -285,6 +367,7 @@ export function buildReceiptModel(sessionId, events, extras) {
|
|
|
285
367
|
discovery: extras?.discovery,
|
|
286
368
|
investigate: extras?.investigate,
|
|
287
369
|
alerts: extras?.alerts,
|
|
370
|
+
incidentAlerts: extras?.incidentAlerts,
|
|
288
371
|
otel: extras?.otel,
|
|
289
372
|
ioc: extras?.ioc,
|
|
290
373
|
posture: extras?.posture,
|
|
@@ -304,6 +387,7 @@ export function buildWindowReceiptModel(events, days, extras) {
|
|
|
304
387
|
liveToken: extras?.liveToken,
|
|
305
388
|
searchQuery: extras?.searchQuery,
|
|
306
389
|
identity: extras?.identity,
|
|
390
|
+
planeConfigured: extras?.planeConfigured,
|
|
307
391
|
sandboxes: extras?.sandboxes,
|
|
308
392
|
openshell: extras?.openshell,
|
|
309
393
|
wiredHosts: extras?.wiredHosts,
|
|
@@ -315,6 +399,7 @@ export function buildWindowReceiptModel(events, days, extras) {
|
|
|
315
399
|
discovery: extras?.discovery,
|
|
316
400
|
investigate: extras?.investigate,
|
|
317
401
|
alerts: extras?.alerts,
|
|
402
|
+
incidentAlerts: extras?.incidentAlerts,
|
|
318
403
|
otel: extras?.otel,
|
|
319
404
|
ioc: extras?.ioc,
|
|
320
405
|
posture: extras?.posture,
|
|
@@ -339,6 +424,7 @@ export function loadReceiptModel(input) {
|
|
|
339
424
|
liveToken: input.liveToken,
|
|
340
425
|
searchQuery: input.searchQuery,
|
|
341
426
|
identity: loadIdentity(input.cwd),
|
|
427
|
+
planeConfigured: isPlaneConfigured(loadIdentity(input.cwd), env),
|
|
342
428
|
sandboxes: loadSandboxes(input.cwd),
|
|
343
429
|
openshell: loadOpenshellCard({ cwd: input.cwd, events: importEvents, env, certify }),
|
|
344
430
|
wiredHosts: loadWiredHosts(input.cwd),
|
|
@@ -350,6 +436,7 @@ export function loadReceiptModel(input) {
|
|
|
350
436
|
discovery: loadDiscoveryCard(env),
|
|
351
437
|
investigate: loadInvestigateLastRun(env),
|
|
352
438
|
alerts: summarizeNotifyLog(readNotifyLog(env)),
|
|
439
|
+
incidentAlerts: readAlertsState(env),
|
|
353
440
|
otel: readOtelStats(env),
|
|
354
441
|
ioc: readIocResult(env),
|
|
355
442
|
posture: postureCard(env),
|
|
@@ -375,6 +462,7 @@ function filterDataAttrs(e, sessionId) {
|
|
|
375
462
|
const project = e.project?.trim();
|
|
376
463
|
const server = parseMcpToolId(e.toolId);
|
|
377
464
|
return (`data-verdict="${esc(e.verdict.toUpperCase())}"` +
|
|
465
|
+
(outcomeOf(e) ? ` data-outcome="${outcomeOf(e)}"` : "") +
|
|
378
466
|
(never ? ` data-never="1"` : "") +
|
|
379
467
|
` data-mode="${esc(e.mode)}"` +
|
|
380
468
|
` data-plane="${esc(e.host?.trim() || "unknown")}"` +
|
|
@@ -410,6 +498,18 @@ function noMatchHint(query, env = process.env) {
|
|
|
410
498
|
return (`${base} Plain-language search is off, so only exact words match. ` +
|
|
411
499
|
`Turn it on once with <code>${esc(semanticInstallCommand(env))}</code>, then search again.`);
|
|
412
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* "Make card" next to a stopped row (G5). Only a denied call gets it. The live report gets a button that calls the local
|
|
503
|
+
* /action/card; a static file has no server, so it shows the CLI command. Never in the deny text the agent reads.
|
|
504
|
+
*/
|
|
505
|
+
function cardControl(e, live) {
|
|
506
|
+
if (!isNearMiss(e))
|
|
507
|
+
return "";
|
|
508
|
+
const id = esc(eventIdOf(e));
|
|
509
|
+
return live
|
|
510
|
+
? `<span class="card-ctl"><button type="button" class="stat" data-card-id="${id}">Make card</button><span class="mute small card-result" role="status" aria-live="polite"></span></span>`
|
|
511
|
+
: `<span class="card-ctl mute small">Make card: <code>kya receipt --card --id ${id}</code></span>`;
|
|
512
|
+
}
|
|
413
513
|
function renderFeed(events, nowMs, live, preserveOrder, noMatch) {
|
|
414
514
|
if (events.length === 0) {
|
|
415
515
|
if (noMatch)
|
|
@@ -419,19 +519,34 @@ function renderFeed(events, nowMs, live, preserveOrder, noMatch) {
|
|
|
419
519
|
: "No events yet - evaluate a tool call with <code>kya wrap</code>, then regenerate this receipt.";
|
|
420
520
|
return `<p class="empty">${hint}</p>`;
|
|
421
521
|
}
|
|
422
|
-
const ordered = preserveOrder ? [...events] : [...events].sort(
|
|
522
|
+
const ordered = preserveOrder ? [...events] : [...events].sort(compareFeed);
|
|
523
|
+
// Search ranking keeps its order and never collapses; the plain feed merges identical consecutive rows.
|
|
524
|
+
const rows = preserveOrder ? ordered.map((event) => ({ event, count: 1 })) : collapseFeedRows(ordered);
|
|
423
525
|
const parts = [];
|
|
424
526
|
let lastDay = "";
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
527
|
+
let pinnedHeading = false;
|
|
528
|
+
for (const { event: e, count } of rows) {
|
|
529
|
+
// Blocked rows sit above everything else, under one heading, so they never hide below repeated rows.
|
|
530
|
+
const pinned = !preserveOrder && outcomeOf(e) === "blocked";
|
|
531
|
+
if (pinned) {
|
|
532
|
+
if (!pinnedHeading) {
|
|
533
|
+
pinnedHeading = true;
|
|
534
|
+
parts.push(`<div class="day" role="presentation">Blocked</div>`);
|
|
535
|
+
}
|
|
430
536
|
}
|
|
431
|
-
|
|
537
|
+
else {
|
|
538
|
+
const key = localDayKey(e.ts);
|
|
539
|
+
if (key !== lastDay) {
|
|
540
|
+
lastDay = key;
|
|
541
|
+
parts.push(`<div class="day" role="presentation">${esc(dayLabel(key, nowMs))}</div>`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const t = rowTone(e);
|
|
432
545
|
const never = e.neverEvent || e.reasonCode === "NEVER_EVENT";
|
|
433
|
-
const
|
|
434
|
-
|
|
546
|
+
const note = ranNote(e);
|
|
547
|
+
const summaryText = count > 1 ? repeatText(e, count) : e.summary?.trim();
|
|
548
|
+
const summary = summaryText
|
|
549
|
+
? `<div class="summary">${esc(summaryText)}</div>`
|
|
435
550
|
: "";
|
|
436
551
|
const preview = e.diffPreview?.trim()
|
|
437
552
|
? `<details class="diff"><summary>Change preview</summary><pre>${esc(e.diffPreview.trim())}</pre></details>`
|
|
@@ -446,9 +561,10 @@ function renderFeed(events, nowMs, live, preserveOrder, noMatch) {
|
|
|
446
561
|
<div class="rail" aria-hidden="true"><span class="tick"></span></div>
|
|
447
562
|
<div class="main">
|
|
448
563
|
<div class="top">
|
|
449
|
-
<span class="verdict">${esc(
|
|
564
|
+
<span class="verdict">${esc(rowWord(e))}</span>${note ? `<span class="strict-note">${esc(note)}</span>` : ""}${modeBadge(e.mode)}
|
|
450
565
|
<code class="tool">${esc(e.toolId)}</code>
|
|
451
566
|
<time class="when" datetime="${esc(e.ts)}" title="${esc(e.ts)}">${esc(clock(e.ts))} <span class="rel">${esc(relativeTime(e.ts, nowMs))}</span></time>
|
|
567
|
+
${cardControl(e, live)}
|
|
452
568
|
</div>
|
|
453
569
|
${summary}
|
|
454
570
|
${preview}
|
|
@@ -484,11 +600,11 @@ function changesPanel(changes, nowMs) {
|
|
|
484
600
|
.map((f) => {
|
|
485
601
|
const entries = f.entries
|
|
486
602
|
.map((e) => {
|
|
487
|
-
const t =
|
|
603
|
+
const t = rowTone(e);
|
|
488
604
|
const dataAttrs = filterDataAttrs(e, s.sessionId);
|
|
489
605
|
return ` <div class="chg-entry ${t}${e.never ? " never" : ""}" ${dataAttrs}>
|
|
490
606
|
<div class="top">
|
|
491
|
-
<span class="verdict">${esc(
|
|
607
|
+
<span class="verdict">${esc(rowWord(e))}</span>${modeBadge(e.mode)}
|
|
492
608
|
<code class="tool">${esc(e.toolId)}</code>
|
|
493
609
|
<time class="when" datetime="${esc(e.ts)}" title="${esc(e.ts)}">${esc(clock(e.ts))} <span class="rel">${esc(relativeTime(e.ts, nowMs))}</span></time>
|
|
494
610
|
</div>
|
|
@@ -517,13 +633,41 @@ ${files}
|
|
|
517
633
|
})
|
|
518
634
|
.join("\n");
|
|
519
635
|
}
|
|
520
|
-
|
|
636
|
+
/** Default base URL `kya init` writes. It is a placeholder, not a plane that runs. */
|
|
637
|
+
const DEFAULT_PLANE_URL = "http://127.0.0.1:8090";
|
|
638
|
+
/** A control plane counts as configured with an API key, saved hosted login, or a base URL other than the init default. */
|
|
639
|
+
export function isPlaneConfigured(identity, env = process.env) {
|
|
640
|
+
if (env.KYA_API_KEY?.trim() || env.SHIELD_API_KEY?.trim() || env.KYA_BASE_URL?.trim())
|
|
641
|
+
return true;
|
|
642
|
+
if (readCredentials(env))
|
|
643
|
+
return true;
|
|
644
|
+
const base = identity?.baseUrl?.trim().replace(/\/+$/, "");
|
|
645
|
+
return Boolean(base && base !== DEFAULT_PLANE_URL);
|
|
646
|
+
}
|
|
647
|
+
/** Who the report is for. Host and plane address show only when a plane is configured. */
|
|
648
|
+
function identityBits(identity, planeConfigured) {
|
|
521
649
|
if (!identity)
|
|
522
|
-
return
|
|
650
|
+
return [];
|
|
651
|
+
// Hidden fields are still checked: a credential in the saved base URL must stop the render, shown or not.
|
|
652
|
+
assertNoSecrets([identity.agentName, identity.agentId, identity.host, identity.baseUrl].filter(Boolean).join(" · "));
|
|
523
653
|
const nameId = identity.agentName && identity.agentId
|
|
524
654
|
? `${identity.agentName}/${identity.agentId}`
|
|
525
655
|
: (identity.agentName ?? identity.agentId);
|
|
526
|
-
|
|
656
|
+
return [nameId, ...(planeConfigured ? [identity.host, identity.baseUrl] : [])].filter((b) => typeof b === "string" && b.trim().length > 0);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Header block of the live report (F4): the "Share this report" button and the place for its result. The button
|
|
660
|
+
* calls POST /action/share (the same redacted payload as `kya receipt --share`). Static files do not get it:
|
|
661
|
+
* there is no server to answer.
|
|
662
|
+
*/
|
|
663
|
+
function shareHeaderBlock() {
|
|
664
|
+
return `<div class="share-row" id="share-report" style="margin-top:0.75rem">
|
|
665
|
+
<button type="button" class="stat" id="share-report-btn">Share this report</button>
|
|
666
|
+
<div id="share-report-result" role="status" aria-live="polite"></div>
|
|
667
|
+
</div>`;
|
|
668
|
+
}
|
|
669
|
+
function identityLine(identity, planeConfigured) {
|
|
670
|
+
const bits = identityBits(identity, planeConfigured);
|
|
527
671
|
if (bits.length === 0)
|
|
528
672
|
return "";
|
|
529
673
|
// baseUrl is inert text here - never an anchor.
|
|
@@ -641,7 +785,20 @@ function dashboardPanel(db) {
|
|
|
641
785
|
</button>`;
|
|
642
786
|
// Zero-count rows are hidden, consistent with the header chips.
|
|
643
787
|
const mixRow = (label, group, value, count, fill) => count > 0 ? barRow(label, group, value, pct(count), `${pct(count)}%`, fill) : "";
|
|
644
|
-
const
|
|
788
|
+
const om = db.outcomeMix;
|
|
789
|
+
const mixCard = om.recorded > 0
|
|
790
|
+
? `<div class="db-card">
|
|
791
|
+
<h3>Outcome mix</h3>
|
|
792
|
+
${mixRow("Ran", "outcome", "ran", om.ran, "ok")}
|
|
793
|
+
${mixRow("Blocked", "outcome", "blocked", om.blocked, "bad")}
|
|
794
|
+
${mixRow("Held", "outcome", "held", om.held, "warn")}
|
|
795
|
+
${mixRow("Error", "outcome", "error", om.error, "warn")}
|
|
796
|
+
${mixRow("Earlier: Allow", "verdict", "ALLOW", om.legacyMix.allow, "ok")}
|
|
797
|
+
${mixRow("Earlier: Review", "verdict", "REQUIRE_APPROVE", om.legacyMix.review, "warn")}
|
|
798
|
+
${mixRow("Earlier: Deny", "verdict", "DENY", om.legacyMix.deny, "bad")}
|
|
799
|
+
${mixRow("Never", "never", "1", db.verdictMix.never, "bad")}
|
|
800
|
+
</div>`
|
|
801
|
+
: `<div class="db-card">
|
|
645
802
|
<h3>Verdict mix</h3>
|
|
646
803
|
${mixRow("Allow", "verdict", "ALLOW", db.verdictMix.allow, "ok")}
|
|
647
804
|
${mixRow("Review", "verdict", "REQUIRE_APPROVE", db.verdictMix.review, "warn")}
|
|
@@ -1374,6 +1531,46 @@ function kpiTile(label, value, cls = "") {
|
|
|
1374
1531
|
const toned = typeof value === "number" && value > 0 && cls ? ` ${cls}` : "";
|
|
1375
1532
|
return `<div class="kpi"><span class="kpi-num${toned}">${v}</span><span class="kpi-lab">${label}</span></div>`;
|
|
1376
1533
|
}
|
|
1534
|
+
/** Rows the hook stopped (outcome "blocked", F3), newest first. Rows with no outcome are not counted. */
|
|
1535
|
+
export function blockedCalls(events) {
|
|
1536
|
+
return events.filter((e) => outcomeOf(e) === "blocked").sort((a, b) => b.ts.localeCompare(a.ts));
|
|
1537
|
+
}
|
|
1538
|
+
const BLOCKED_ROWS_SHOWN = 5;
|
|
1539
|
+
/**
|
|
1540
|
+
* First panel of the report (guard first): how many calls the guard stopped and the last five.
|
|
1541
|
+
* With none blocked it shows the three built-in demo calls, labelled as a demo, never as activity.
|
|
1542
|
+
*/
|
|
1543
|
+
function heroBlockedPanel(events, nowMs, windowLabel, live) {
|
|
1544
|
+
const blocked = blockedCalls(events);
|
|
1545
|
+
if (blocked.length === 0) {
|
|
1546
|
+
const demo = DEMO_CALLS.map((c) => ` <li><code>${esc(c.label)}</code><span class="blk-what">${esc(c.what)}</span><span class="pill">demo</span></li>`).join("\n");
|
|
1547
|
+
return `<section class="panel hero-blocked demo" id="frag-hero-blocked" aria-label="Blocked">
|
|
1548
|
+
<h2>Blocked <span class="pill">demo</span></h2>
|
|
1549
|
+
<p class="blk-lead"><span class="blk-num">0</span> calls blocked ${esc(windowLabel)}. Demo: three calls your agent could make, and the guard stops all three.</p>
|
|
1550
|
+
<ul class="blk-rows">
|
|
1551
|
+
${demo}
|
|
1552
|
+
</ul>
|
|
1553
|
+
<p class="mute small">These rows are samples, not your activity. Real blocked calls replace them here.</p>
|
|
1554
|
+
</section>`;
|
|
1555
|
+
}
|
|
1556
|
+
const rows = blocked
|
|
1557
|
+
.slice(0, BLOCKED_ROWS_SHOWN)
|
|
1558
|
+
.map((e) => {
|
|
1559
|
+
const what = e.summary?.trim() ? esc(clip(e.summary.trim(), 140)) : "";
|
|
1560
|
+
const rule = e.neverRule ? `<span class="mute small">rule <code>${esc(e.neverRule)}</code></span>` : "";
|
|
1561
|
+
return ` <li><code>${esc(e.toolId)}</code><span class="blk-what">${what}</span>${rule}<span class="mute small">${esc(relativeTime(e.ts, nowMs))}</span>${cardControl(e, live)}</li>`;
|
|
1562
|
+
})
|
|
1563
|
+
.join("\n");
|
|
1564
|
+
const more = blocked.length > BLOCKED_ROWS_SHOWN ? `<p class="mute small">Showing the last ${BLOCKED_ROWS_SHOWN} of ${blocked.length}. The Activity tab lists every call.</p>` : "";
|
|
1565
|
+
return `<section class="panel hero-blocked" id="frag-hero-blocked" aria-label="Blocked">
|
|
1566
|
+
<h2>Blocked</h2>
|
|
1567
|
+
<p class="blk-lead"><span class="blk-num bad">${blocked.length}</span> ${blocked.length === 1 ? "call" : "calls"} blocked ${esc(windowLabel)}.</p>
|
|
1568
|
+
<ul class="blk-rows">
|
|
1569
|
+
${rows}
|
|
1570
|
+
</ul>
|
|
1571
|
+
${more}
|
|
1572
|
+
</section>`;
|
|
1573
|
+
}
|
|
1377
1574
|
/**
|
|
1378
1575
|
* Hero Certify panel: purpose-built rich card for the live Agent Trust
|
|
1379
1576
|
* Baseline. The section carries the result state (`hero-certify pass|gap|
|
|
@@ -1536,6 +1733,23 @@ function verdictsHeroCard(c, total) {
|
|
|
1536
1733
|
// raw event count keeps every bar inside the base the Overview card uses.
|
|
1537
1734
|
const pct = (n) => (total > 0 ? Math.round((n / total) * 100) : 0);
|
|
1538
1735
|
const mixRow = (label, n, fill) => `<div class="mix-row"><span class="mix-lab">${label}</span><span class="mix-bar"><span class="mix-fill ${fill}" style="width:${pct(n)}%"></span></span><span class="mix-num">${n}</span></div>`;
|
|
1736
|
+
const o = c.outcomes;
|
|
1737
|
+
if (o.recorded > 0) {
|
|
1738
|
+
const row = (label, n, fill) => (n > 0 ? mixRow(label, n, fill) : "");
|
|
1739
|
+
return `<section class="panel" aria-label="Verdicts">
|
|
1740
|
+
<h2>Decisions</h2>
|
|
1741
|
+
<div class="mix">
|
|
1742
|
+
${mixRow("Ran", o.ran, "ok")}
|
|
1743
|
+
${mixRow("Blocked", o.blocked, "bad")}
|
|
1744
|
+
${mixRow("Held", o.held, "warn")}
|
|
1745
|
+
${row("Error", o.error, "warn")}
|
|
1746
|
+
${row("Earlier: Allow", c.allow, "ok")}
|
|
1747
|
+
${row("Earlier: Deny", c.deny, "bad")}
|
|
1748
|
+
${row("Earlier: Review", c.require, "warn")}
|
|
1749
|
+
${mixRow("Never", c.never, "bad")}
|
|
1750
|
+
</div>
|
|
1751
|
+
</section>`;
|
|
1752
|
+
}
|
|
1539
1753
|
return `<section class="panel" aria-label="Verdicts">
|
|
1540
1754
|
<h2>Decisions</h2>
|
|
1541
1755
|
<div class="mix">
|
|
@@ -1769,6 +1983,29 @@ ${card.findings
|
|
|
1769
1983
|
<p class="mute small">ran ${esc(relativeTime(card.ranAt, nowMs))} · window ${card.windowDays}d · <code>kya investigate</code> for the full report</p>
|
|
1770
1984
|
</section>`;
|
|
1771
1985
|
}
|
|
1986
|
+
/** Overview list of incidents that alerted, newest first. Renders nothing until one has alerted. */
|
|
1987
|
+
function incidentAlertsPanel(entries, nowMs) {
|
|
1988
|
+
if (!entries || entries.length === 0)
|
|
1989
|
+
return "";
|
|
1990
|
+
const rows = [...entries]
|
|
1991
|
+
.reverse()
|
|
1992
|
+
.slice(0, 5)
|
|
1993
|
+
.map((e) => {
|
|
1994
|
+
const where = e.status === "sent" && e.delivered > 0 ? `sent to ${e.delivered}` : e.status === "sent" ? "report only" : "not delivered";
|
|
1995
|
+
const tri = e.triage
|
|
1996
|
+
? `<br><span class="mute small">triage (local model, advisory): ${esc(e.triage.category.id)} ${Math.round(e.triage.category.p * 100)}% · ${esc(e.triage.urgency.id)} · ${esc(e.triage.action.id)} · harmless ${Math.round(e.triage.benign * 100)}%</span>`
|
|
1997
|
+
: "";
|
|
1998
|
+
return ` <li>${severityPill(e.severity)} ${esc(e.title)} <span class="mute">${esc(where)} · <time datetime="${esc(e.ts)}" title="${esc(e.ts)}">${esc(relativeTime(e.ts, nowMs))}</time></span>${tri}</li>`;
|
|
1999
|
+
})
|
|
2000
|
+
.join("\n");
|
|
2001
|
+
return `<section class="panel" aria-label="Incident alerts">
|
|
2002
|
+
<h2>Incident alerts</h2>
|
|
2003
|
+
<ul class="rows">
|
|
2004
|
+
${rows}
|
|
2005
|
+
</ul>
|
|
2006
|
+
<p class="mute small">incidents that raised an alert · <code>kya alerts status</code> · local KYA is advisory</p>
|
|
2007
|
+
</section>`;
|
|
2008
|
+
}
|
|
1772
2009
|
/**
|
|
1773
2010
|
* System-tab Alerts panel: per-target delivery rollup from the notify ledger.
|
|
1774
2011
|
* With no deliveries yet the panel still renders as an empty state (config
|
|
@@ -1877,7 +2114,7 @@ export function renderFeedPage(events, filters, page, nowMs, live, query, embedd
|
|
|
1877
2114
|
total = ranked.length;
|
|
1878
2115
|
}
|
|
1879
2116
|
else {
|
|
1880
|
-
const sorted = [...events].sort(
|
|
2117
|
+
const sorted = [...events].sort(compareFeed);
|
|
1881
2118
|
ordered = applyChipFilters(sorted, filters);
|
|
1882
2119
|
total = ordered.length;
|
|
1883
2120
|
}
|
|
@@ -1921,6 +2158,21 @@ const zoneBody = (parts, empty) => parts.some((p) => p.length > 0)
|
|
|
1921
2158
|
: `<p class="mute small zone-empty">${empty}</p>`;
|
|
1922
2159
|
/** Inner markup of the Activity counts bar (verdict buttons + mode/plane chips). */
|
|
1923
2160
|
function statCountsInner(c, chips) {
|
|
2161
|
+
const o = c.outcomes;
|
|
2162
|
+
if (o.recorded > 0) {
|
|
2163
|
+
const btn = (label, n, group, value, cls, title) => `<button type="button" class="stat${cls ? ` ${cls}` : ""}" data-fgroup="${group}" data-fvalue="${value}" aria-pressed="false" title="Filter: ${title}">${label}<b>${n}</b></button>`;
|
|
2164
|
+
// Rows from before outcomes existed keep their verdict counts, labelled as earlier rows.
|
|
2165
|
+
const earlier = (label, n, value, cls) => n > 0 ? btn(`Earlier: ${label}`, n, "verdict", value, cls, `${label} (rows with no outcome recorded)`) : "";
|
|
2166
|
+
const howTo = o.wouldHold > 0 ? `\n <span class="strict-howto">${strictHowTo(o.wouldHold, (t) => `<code>${t}</code>`)}</span>` : "";
|
|
2167
|
+
return `${btn("Ran", o.ran, "outcome", "ran", "ok", "Ran")}
|
|
2168
|
+
${btn("Blocked", o.blocked, "outcome", "blocked", "bad", "Blocked")}
|
|
2169
|
+
${btn("Held", o.held, "outcome", "held", "warn", "Held")}
|
|
2170
|
+
${o.error > 0 ? btn("Error", o.error, "outcome", "error", "", "Error") : ""}
|
|
2171
|
+
${earlier("Allow", c.allow, "ALLOW", "ok")}${earlier("Deny", c.deny, "DENY", "bad")}${earlier("Review", c.require, "REQUIRE_APPROVE", "warn")}
|
|
2172
|
+
<button type="button" class="stat" data-fgroup="never" data-fvalue="1" aria-pressed="false" title="Filter: Never">Never<b>${c.never}</b></button>
|
|
2173
|
+
${chips.modes}${chips.planes}${howTo}
|
|
2174
|
+
<button type="button" class="stat clear-filters" id="clear-filters" hidden>Clear filters ×</button>`;
|
|
2175
|
+
}
|
|
1924
2176
|
return `<button type="button" class="stat ok" data-fgroup="verdict" data-fvalue="ALLOW" aria-pressed="false" title="Filter: Allow">Allow<b>${c.allow}</b></button>
|
|
1925
2177
|
<button type="button" class="stat bad" data-fgroup="verdict" data-fvalue="DENY" aria-pressed="false" title="Filter: Deny">Deny<b>${c.deny}</b></button>
|
|
1926
2178
|
<button type="button" class="stat warn" data-fgroup="verdict" data-fvalue="REQUIRE_APPROVE" aria-pressed="false" title="Filter: Review">Review<b>${c.require}</b></button>
|
|
@@ -1948,7 +2200,7 @@ export function buildLiveFragments(model, livePaged) {
|
|
|
1948
2200
|
chipProducts: chips.products,
|
|
1949
2201
|
chipProjects: chips.projects,
|
|
1950
2202
|
chipServers: chips.servers,
|
|
1951
|
-
|
|
2203
|
+
heroBlocked: heroBlockedPanel(events, nowMs, `in the ${model.rangeLabel}`, model.live),
|
|
1952
2204
|
heroTiles: [
|
|
1953
2205
|
verdictsHeroCard(c, events.length),
|
|
1954
2206
|
activityHeroCard(agg, dashboard, events.length),
|
|
@@ -1956,18 +2208,24 @@ export function buildLiveFragments(model, livePaged) {
|
|
|
1956
2208
|
importHeroCard(model.imports, nowMs, livePaged),
|
|
1957
2209
|
discoveryHeroCard(model.discovery, nowMs, livePaged),
|
|
1958
2210
|
investigateHeroCard(model.investigate, nowMs, livePaged),
|
|
1959
|
-
showbackHeroCard(model.showback, events),
|
|
1960
2211
|
].join("\n "),
|
|
1961
|
-
|
|
1962
|
-
|
|
2212
|
+
showbackPanel: showbackHeroCard(model.showback, events),
|
|
2213
|
+
connectedBody: zoneBody([wiredHostsPanel(model.wiredHosts)], "No coding agent is connected yet. Run kya connect <host> to add one."),
|
|
2214
|
+
overviewPanels: zoneBody([incidentAlertsPanel(model.incidentAlerts, nowMs), dashboardPanel(dashboard), sessionsPanel(agg, nowMs, changes), reasonsPanel(agg)], "No activity in this window yet - analytics appear once events land."),
|
|
2215
|
+
certifyBody: zoneBody([
|
|
2216
|
+
heroCertifyPanel(model.certify),
|
|
2217
|
+
model.certify
|
|
2218
|
+
? certifyDetailPanel(model.certify.requirements)
|
|
2219
|
+
: `<p class="mute small zone-empty">The full rule table appears here after the first check.</p>`,
|
|
2220
|
+
], "No check yet - use your coding tools with kya connected, then run kya certify."),
|
|
1963
2221
|
orrPanel: orrPanel(model.orr, nowMs, livePaged),
|
|
1964
2222
|
changesList: changesPanel(changes, nowMs),
|
|
1965
2223
|
},
|
|
1966
2224
|
nav: {
|
|
1967
|
-
overview: events.length,
|
|
2225
|
+
overview: blockedCalls(events).length,
|
|
1968
2226
|
activity: events.length,
|
|
1969
2227
|
changes: changes.fileCount,
|
|
1970
|
-
certify:
|
|
2228
|
+
certify: null,
|
|
1971
2229
|
orr: model.orr ? model.orr.scorecards.fail : null,
|
|
1972
2230
|
},
|
|
1973
2231
|
};
|
|
@@ -2371,8 +2629,10 @@ export function renderReceiptHtml(model) {
|
|
|
2371
2629
|
chipProducts: 'chip-products',
|
|
2372
2630
|
chipProjects: 'chip-projects',
|
|
2373
2631
|
chipServers: 'chip-servers',
|
|
2374
|
-
|
|
2632
|
+
heroBlocked: 'frag-hero-blocked',
|
|
2375
2633
|
heroTiles: 'frag-hero-tiles',
|
|
2634
|
+
showbackPanel: 'frag-showback',
|
|
2635
|
+
connectedBody: 'frag-connected',
|
|
2376
2636
|
overviewPanels: 'frag-overview-panels',
|
|
2377
2637
|
certifyBody: 'frag-certify',
|
|
2378
2638
|
orrPanel: 'frag-orr',
|
|
@@ -2589,6 +2849,25 @@ export function renderReceiptHtml(model) {
|
|
|
2589
2849
|
try { localStorage.setItem('kya-theme', root.getAttribute('data-theme')); } catch(e){}
|
|
2590
2850
|
});
|
|
2591
2851
|
})();
|
|
2852
|
+
</script>`;
|
|
2853
|
+
// The "More" menu: closed by default, remembered per browser, and open when the page was reached through
|
|
2854
|
+
// a link to a tab inside it (a hash such as #certify). Without JS or localStorage the page still works.
|
|
2855
|
+
const moreScript = `<script>
|
|
2856
|
+
(function(){
|
|
2857
|
+
var more = document.getElementById('nav-more');
|
|
2858
|
+
if (!more) return;
|
|
2859
|
+
var inside = false;
|
|
2860
|
+
try { inside = !!(location.hash && more.querySelector('a[href="' + location.hash + '"]')); } catch(e){}
|
|
2861
|
+
var stored = null;
|
|
2862
|
+
try { stored = localStorage.getItem('kya-nav-more'); } catch(e){}
|
|
2863
|
+
if (inside || stored === 'open') more.open = true;
|
|
2864
|
+
more.addEventListener('toggle', function(){
|
|
2865
|
+
try { localStorage.setItem('kya-nav-more', more.open ? 'open' : 'closed'); } catch(e){}
|
|
2866
|
+
});
|
|
2867
|
+
window.addEventListener('hashchange', function(){
|
|
2868
|
+
try { if (location.hash && more.querySelector('a[href="' + location.hash + '"]')) more.open = true; } catch(e){}
|
|
2869
|
+
});
|
|
2870
|
+
})();
|
|
2592
2871
|
</script>`;
|
|
2593
2872
|
// Gateway interactive controls: playground evaluator, quick actions, and the
|
|
2594
2873
|
// sidebar stop-report link. Only wired when the page is served by the live
|
|
@@ -2748,6 +3027,68 @@ export function renderReceiptHtml(model) {
|
|
|
2748
3027
|
});
|
|
2749
3028
|
});
|
|
2750
3029
|
|
|
3030
|
+
// The card control next to a stopped row. Delegated: the Blocked panel is replaced on every live refresh.
|
|
3031
|
+
document.addEventListener('click', function(ev){
|
|
3032
|
+
var b = ev.target && ev.target.closest ? ev.target.closest('[data-card-id]') : null;
|
|
3033
|
+
if (!b) return;
|
|
3034
|
+
var out = b.parentNode.querySelector('.card-result');
|
|
3035
|
+
b.disabled = true;
|
|
3036
|
+
out.textContent = 'Making the card...';
|
|
3037
|
+
post('/action/card', { id: b.getAttribute('data-card-id') }).then(parseJson).then(function(d){
|
|
3038
|
+
out.textContent = d && d.ok ? 'Saved ' + (d.png || d.svg) + '. Caption: ' + d.caption.replace(/\\n/g, ' ') : 'No card: ' + (d && d.error ? d.error : 'unknown error');
|
|
3039
|
+
b.disabled = false;
|
|
3040
|
+
}).catch(function(err){ out.textContent = 'No card: ' + err.message; b.disabled = false; });
|
|
3041
|
+
});
|
|
3042
|
+
|
|
3043
|
+
var shareBtn = document.getElementById('share-report-btn');
|
|
3044
|
+
if (shareBtn) {
|
|
3045
|
+
var shareOut = document.getElementById('share-report-result');
|
|
3046
|
+
var shareFail = function(msg){
|
|
3047
|
+
setResult(shareOut, '<p>Could not share the report: ' + esc(msg) + '. The report still works. Try again when you are online.</p>', true);
|
|
3048
|
+
shareBtn.disabled = false;
|
|
3049
|
+
};
|
|
3050
|
+
var copyText = function(text, field, btn){
|
|
3051
|
+
var done = function(){ btn.textContent = 'Copied'; };
|
|
3052
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
3053
|
+
navigator.clipboard.writeText(text).then(done, function(){ field.select(); btn.textContent = 'Press Ctrl+C to copy'; });
|
|
3054
|
+
} else { field.select(); btn.textContent = 'Press Ctrl+C to copy'; }
|
|
3055
|
+
};
|
|
3056
|
+
shareBtn.addEventListener('click', function(){
|
|
3057
|
+
shareBtn.disabled = true;
|
|
3058
|
+
shareOut.textContent = 'Sharing...';
|
|
3059
|
+
post('/action/share', {}).then(parseJson).then(function(d){
|
|
3060
|
+
if (!d || !d.ok) { shareFail(d && d.error ? d.error : 'unknown error'); return; }
|
|
3061
|
+
// Show and open only an http(s) link. share.ts already checked it is on the server that was asked.
|
|
3062
|
+
if (!/^https?:/.test(String(d.url))) { shareFail('the server sent a link that is not a web address'); return; }
|
|
3063
|
+
shareOut.textContent = '';
|
|
3064
|
+
var label = document.createElement('p');
|
|
3065
|
+
label.textContent = 'Public link. Send it to a colleague:';
|
|
3066
|
+
var field = document.createElement('input');
|
|
3067
|
+
field.type = 'text'; field.readOnly = true; field.id = 'share-url'; field.value = d.url; field.size = 44;
|
|
3068
|
+
var copy = document.createElement('button');
|
|
3069
|
+
copy.type = 'button'; copy.className = 'stat'; copy.id = 'share-copy'; copy.textContent = 'Copy link';
|
|
3070
|
+
copy.addEventListener('click', function(){ copyText(d.url, field, copy); });
|
|
3071
|
+
shareOut.appendChild(label); shareOut.appendChild(field); shareOut.appendChild(copy);
|
|
3072
|
+
var claim = document.createElement('p');
|
|
3073
|
+
var a = document.createElement('a');
|
|
3074
|
+
a.id = 'share-claim'; a.href = d.url; a.target = '_blank'; a.rel = 'noopener';
|
|
3075
|
+
a.textContent = 'Open the page to claim it, keep it and get a weekly summary';
|
|
3076
|
+
claim.appendChild(a);
|
|
3077
|
+
shareOut.appendChild(claim);
|
|
3078
|
+
var connect = document.createElement('p');
|
|
3079
|
+
connect.id = 'share-connect';
|
|
3080
|
+
connect.textContent = 'To connect this computer to your account, run kya login.';
|
|
3081
|
+
shareOut.appendChild(connect);
|
|
3082
|
+
if (d.expiresAt) {
|
|
3083
|
+
var exp = document.createElement('p');
|
|
3084
|
+
exp.textContent = 'If nobody claims the report, it is deleted on ' + String(d.expiresAt).slice(0, 10) + '.';
|
|
3085
|
+
shareOut.appendChild(exp);
|
|
3086
|
+
}
|
|
3087
|
+
shareBtn.disabled = false;
|
|
3088
|
+
}).catch(function(err){ shareFail(err.message); });
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
|
|
2751
3092
|
var importForm = document.getElementById('import-form');
|
|
2752
3093
|
if (importForm) {
|
|
2753
3094
|
importForm.addEventListener('submit', function(e){
|
|
@@ -2813,7 +3154,6 @@ export function renderReceiptHtml(model) {
|
|
|
2813
3154
|
postureSectionHtml(model.posture, !!model.liveToken),
|
|
2814
3155
|
capabilitiesPanel(model),
|
|
2815
3156
|
gatePanel(model.gate, nowMs),
|
|
2816
|
-
wiredHostsPanel(model.wiredHosts),
|
|
2817
3157
|
sandboxesPanel(model.sandboxes, nowMs),
|
|
2818
3158
|
openshellPanel(model.openshell, nowMs),
|
|
2819
3159
|
alertsPanel(model.alerts, nowMs, livePaged),
|
|
@@ -2825,7 +3165,7 @@ export function renderReceiptHtml(model) {
|
|
|
2825
3165
|
<head>
|
|
2826
3166
|
<meta charset="utf-8"/>
|
|
2827
3167
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
2828
|
-
<title>${esc(
|
|
3168
|
+
<title>${esc(PROMISE)} - ${esc(model.rangeLabel)}</title>
|
|
2829
3169
|
<style>${receiptCss()}${model.liveToken ? MAP_CSS + MAP_LIVE_ZONE_CSS : ""}</style>
|
|
2830
3170
|
</head>
|
|
2831
3171
|
<body>
|
|
@@ -2837,20 +3177,25 @@ export function renderReceiptHtml(model) {
|
|
|
2837
3177
|
</div>
|
|
2838
3178
|
</header>
|
|
2839
3179
|
<nav class="sidebar-nav" aria-label="Report sections">
|
|
2840
|
-
${navItem("#overview", "
|
|
3180
|
+
${navItem("#overview", "Blocked", blockedCalls(events).length)}
|
|
2841
3181
|
${navItem("#activity", "Activity", events.length)}
|
|
2842
|
-
${navItem("#
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
3182
|
+
${navItem("#connected", "Connected tools", undefined)}
|
|
3183
|
+
<details class="nav-more" id="nav-more">
|
|
3184
|
+
<summary class="nav-more-summary">More</summary>
|
|
3185
|
+
${navItem("#changes", "Changes", changes.fileCount)}
|
|
3186
|
+
${navItem("#certify", "Certify", undefined)}
|
|
3187
|
+
${navItem("#orr", "ORR", model.orr ? model.orr.scorecards.fail : undefined)}
|
|
3188
|
+
${model.liveToken ? navItem("#map", "Map", undefined) : ""}
|
|
3189
|
+
${model.showback ? navItem("#showback", "Showback", undefined) : ""}
|
|
3190
|
+
${navGroup("Gateway", model.gate?.state === "running")}
|
|
3191
|
+
${navSubItem("#gateway-home", "Home", model.gate?.servers.length ?? 0)}
|
|
3192
|
+
${navSubItem("#gateway-listeners", "Listeners", model.gate?.listeners?.length ?? 0)}
|
|
3193
|
+
${navSubItem("#gateway-routes", "Routes", model.gate?.routes?.length ?? 0)}
|
|
3194
|
+
${navSubItem("#gateway-backends", "Backends", model.gate?.servers.length ?? 0)}
|
|
3195
|
+
${navSubItem("#gateway-policies", "Policies", model.gate?.policySummary?.totalPolicies ?? 0)}
|
|
3196
|
+
${navSubItem("#gateway-playground", "Playground", model.gate?.playgroundSamples?.length ?? 0)}
|
|
3197
|
+
${navItem("#system", "System", undefined)}
|
|
3198
|
+
</details>
|
|
2854
3199
|
</nav>
|
|
2855
3200
|
<footer class="sidebar-footer">
|
|
2856
3201
|
<button type="button" class="theme-toggle" id="theme-toggle" aria-label="Toggle theme">Theme</button>
|
|
@@ -2862,16 +3207,17 @@ export function renderReceiptHtml(model) {
|
|
|
2862
3207
|
${model.liveToken ? '<main data-live-token="1">' : '<main>'}
|
|
2863
3208
|
<header class="page-header">
|
|
2864
3209
|
<div class="title-row">
|
|
2865
|
-
<h1>${esc(
|
|
3210
|
+
<h1>${esc(PROMISE)}</h1>
|
|
2866
3211
|
<div class="range">${esc(model.rangeLabel)}${livePill}</div>
|
|
2867
3212
|
</div>
|
|
2868
|
-
${identityLine(model.identity)}
|
|
3213
|
+
${identityLine(model.identity, model.planeConfigured)}
|
|
3214
|
+
${model.liveToken ? shareHeaderBlock() : ""}
|
|
2869
3215
|
</header>
|
|
2870
3216
|
${blockedBanner}
|
|
2871
3217
|
${iocBanner}
|
|
2872
3218
|
<section class="zone" id="overview" aria-labelledby="tab-overview">
|
|
2873
3219
|
<div class="hero">
|
|
2874
|
-
${fr.fragments.
|
|
3220
|
+
${fr.fragments.heroBlocked}
|
|
2875
3221
|
<div class="hero-tiles" id="frag-hero-tiles">
|
|
2876
3222
|
${fr.fragments.heroTiles}
|
|
2877
3223
|
</div>
|
|
@@ -2897,6 +3243,9 @@ ${initialFeed.feedHtml}
|
|
|
2897
3243
|
${initialFeed.paginationHtml}
|
|
2898
3244
|
</div>
|
|
2899
3245
|
</section>
|
|
3246
|
+
<section class="zone" id="connected" aria-labelledby="tab-connected">
|
|
3247
|
+
<div id="frag-connected">${fr.fragments.connectedBody}</div>
|
|
3248
|
+
</section>
|
|
2900
3249
|
<section class="zone" id="changes" aria-labelledby="tab-changes">
|
|
2901
3250
|
<section class="feed chg" id="changes-list" aria-label="Changes by session and file">
|
|
2902
3251
|
${fr.fragments.changesList}
|
|
@@ -2909,6 +3258,7 @@ ${fr.fragments.changesList}
|
|
|
2909
3258
|
<div id="frag-orr">${fr.fragments.orrPanel}</div>
|
|
2910
3259
|
</section>
|
|
2911
3260
|
${model.liveToken ? '<section class="zone" id="map" aria-labelledby="tab-map"><div id="frag-map"><p class="mute">Loading the map.</p></div></section>' : ""}
|
|
3261
|
+
${model.showback ? `<section class="zone" id="showback" aria-labelledby="tab-showback"><div id="frag-showback">${fr.fragments.showbackPanel}</div></section>` : ""}
|
|
2912
3262
|
${gatePagePanel(model.gate, nowMs)}
|
|
2913
3263
|
<section class="zone" id="system" aria-labelledby="tab-system">
|
|
2914
3264
|
${systemBody}
|
|
@@ -2918,6 +3268,7 @@ ${filterScript}
|
|
|
2918
3268
|
${model.liveToken ? liveMapScripts() : ""}
|
|
2919
3269
|
${staticSearchScript}
|
|
2920
3270
|
${themeScript}
|
|
3271
|
+
${moreScript}
|
|
2921
3272
|
${gateScript}
|
|
2922
3273
|
</body>
|
|
2923
3274
|
</html>`;
|
|
@@ -2925,28 +3276,83 @@ ${gateScript}
|
|
|
2925
3276
|
assertNoSecrets(html);
|
|
2926
3277
|
return html;
|
|
2927
3278
|
}
|
|
3279
|
+
/** "## Blocked" section: the count and the last five blocked calls, or the demo when none was blocked. */
|
|
3280
|
+
function blockedMarkdown(events, nowMs) {
|
|
3281
|
+
const blocked = blockedCalls(events);
|
|
3282
|
+
if (blocked.length === 0) {
|
|
3283
|
+
return [
|
|
3284
|
+
"## Blocked (demo)",
|
|
3285
|
+
"",
|
|
3286
|
+
"0 calls blocked in this window. Demo: three calls your agent could make, and the guard stops all three. These rows are samples, not your activity.",
|
|
3287
|
+
"",
|
|
3288
|
+
...DEMO_CALLS.map((c) => `- \`${mdInline(c.label)}\` - ${mdText(c.what)} (demo)`),
|
|
3289
|
+
"",
|
|
3290
|
+
];
|
|
3291
|
+
}
|
|
3292
|
+
return [
|
|
3293
|
+
"## Blocked",
|
|
3294
|
+
"",
|
|
3295
|
+
`${blocked.length} ${blocked.length === 1 ? "call" : "calls"} blocked in this window. Last ${Math.min(5, blocked.length)}:`,
|
|
3296
|
+
"",
|
|
3297
|
+
...blocked.slice(0, 5).map((e) => {
|
|
3298
|
+
const sum = e.summary?.trim() ? ` - ${mdText(clip(e.summary.trim(), 140))}` : "";
|
|
3299
|
+
return `- \`${mdInline(e.toolId)}\`${sum} - ${mdText(relativeTime(e.ts, nowMs))}`;
|
|
3300
|
+
}),
|
|
3301
|
+
"",
|
|
3302
|
+
];
|
|
3303
|
+
}
|
|
2928
3304
|
export function renderReceiptMarkdown(model) {
|
|
2929
3305
|
const c = countVerdicts(model.events);
|
|
2930
3306
|
const agg = aggregateEvents(model.events);
|
|
2931
3307
|
const nowMs = Date.now();
|
|
3308
|
+
const o = c.outcomes;
|
|
2932
3309
|
const lines = [
|
|
2933
|
-
`# ${mdText(
|
|
3310
|
+
`# ${mdText(PROMISE)}`,
|
|
2934
3311
|
"",
|
|
2935
3312
|
mdText(model.rangeLabel),
|
|
2936
3313
|
"",
|
|
2937
|
-
|
|
3314
|
+
...blockedMarkdown(model.events, nowMs),
|
|
3315
|
+
];
|
|
3316
|
+
if (o.recorded > 0) {
|
|
3317
|
+
// Counted by what happened to each call. Rows from before outcomes existed keep their verdict counts, on a second line.
|
|
3318
|
+
lines.push(`${outcomeHeadline(o)} | Never ${c.never}`, "");
|
|
3319
|
+
if (o.wouldHold > 0)
|
|
3320
|
+
lines.push(strictHowTo(o.wouldHold, (t) => `\`${t}\``), "");
|
|
3321
|
+
if (o.legacy > 0) {
|
|
3322
|
+
lines.push(`Earlier calls (no outcome recorded): Allow ${c.allow} | Deny ${c.deny} | Review ${c.require}`, "");
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
else {
|
|
3326
|
+
lines.push(`Allow ${c.allow} | Deny ${c.deny} | Review ${c.require} | Never ${c.never}`, "");
|
|
3327
|
+
}
|
|
3328
|
+
// Feed rows. With outcomes recorded the feed leads (blocked rows first); without, it stays where it was.
|
|
3329
|
+
const feedLines = [
|
|
3330
|
+
"## Feed",
|
|
3331
|
+
...collapseFeedRows(o.recorded > 0 ? [...model.events].sort(compareFeed) : [...model.events].sort((a, b) => b.ts.localeCompare(a.ts)))
|
|
3332
|
+
.flatMap(({ event: e, count }) => {
|
|
3333
|
+
const text = count > 1 ? repeatText(e, count) : e.summary?.trim();
|
|
3334
|
+
const sum = text ? ` - ${mdText(text)}` : "";
|
|
3335
|
+
const note = ranNote(e);
|
|
3336
|
+
const head = `- **${mdText(rowWord(e))}**${note ? ` (${note})` : ""} \`${mdInline(e.toolId)}\`${sum} - ${productLabel(e.product)} - ${mdText(e.reasonCode)}`;
|
|
3337
|
+
if (!e.diffPreview?.trim())
|
|
3338
|
+
return [head];
|
|
3339
|
+
// Fenced content is verbatim (stripEscapes only - backslash escapes
|
|
3340
|
+
// are literal in code fences); the dynamically sized tilde fence
|
|
3341
|
+
// prevents breakout and backticks cannot close a tilde fence.
|
|
3342
|
+
const diff = stripEscapes(e.diffPreview.trim());
|
|
3343
|
+
const fence = mdFence(diff);
|
|
3344
|
+
return [head, "", fence, diff, fence, ""];
|
|
3345
|
+
}),
|
|
2938
3346
|
"",
|
|
2939
3347
|
];
|
|
3348
|
+
if (o.recorded > 0)
|
|
3349
|
+
lines.push(...feedLines);
|
|
2940
3350
|
if (model.ioc) {
|
|
2941
3351
|
const k = model.ioc.findings.length;
|
|
2942
3352
|
lines.push(mdText(`IoC check (advisory): ${k === 0 ? "no findings" : `${k} finding${k === 1 ? "" : "s"}`}, list ${model.ioc.listVersion}, checked ${model.ioc.checkedAt}.`), "");
|
|
2943
3353
|
}
|
|
2944
3354
|
if (model.identity) {
|
|
2945
|
-
const
|
|
2946
|
-
const nameId = id.agentName && id.agentId
|
|
2947
|
-
? `${id.agentName}/${id.agentId}`
|
|
2948
|
-
: (id.agentName ?? id.agentId);
|
|
2949
|
-
const bits = [nameId, id.host, id.baseUrl].filter((b) => typeof b === "string" && b.trim().length > 0);
|
|
3355
|
+
const bits = identityBits(model.identity, model.planeConfigured);
|
|
2950
3356
|
if (bits.length > 0) {
|
|
2951
3357
|
lines.push(mdText(bits.join(" · ")), "");
|
|
2952
3358
|
}
|
|
@@ -2961,7 +3367,7 @@ export function renderReceiptMarkdown(model) {
|
|
|
2961
3367
|
agg.planes.runtime > 0 ? `Runtime ${agg.planes.runtime}` : "",
|
|
2962
3368
|
].filter(Boolean);
|
|
2963
3369
|
if (modeBits.length > 0 || planeBits.length > 0) {
|
|
2964
|
-
lines.push(`Modes: ${modeBits.join(" | ") || "-"} ·
|
|
3370
|
+
lines.push(`Modes: ${modeBits.join(" | ") || "-"} · Source: ${planeBits.join(" | ") || "-"}`, "");
|
|
2965
3371
|
}
|
|
2966
3372
|
if (agg.products.length >= 2) {
|
|
2967
3373
|
lines.push("## Products", ...agg.products.map((p) => `- ${mdText(p.label)} × ${p.count}`), "");
|
|
@@ -2969,23 +3375,16 @@ export function renderReceiptMarkdown(model) {
|
|
|
2969
3375
|
if (agg.projects.length >= 2) {
|
|
2970
3376
|
lines.push("## Projects", ...agg.projects.map((p) => `- ${mdText(p.label)} × ${p.count}`), "");
|
|
2971
3377
|
}
|
|
2972
|
-
if (model.certify) {
|
|
2973
|
-
const c = model.certify;
|
|
2974
|
-
lines.push("## Certify", `result: ${c.result} · counts: ${c.pass} pass · ${c.gap} gap · ${c.insufficientEvidence} insufficient · ${c.attested} attested`);
|
|
2975
|
-
if (c.topGaps.length > 0) {
|
|
2976
|
-
lines.push("Top gaps:");
|
|
2977
|
-
for (const g of c.topGaps) {
|
|
2978
|
-
lines.push(`- \`${mdInline(g.id)}\` - ${mdText(g.title)} (${mdText(g.severity)})`);
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
|
-
lines.push(`checked live - last ${c.windowDays} days, ${c.trailEvents} recorded events · the Certify tab lists every rule · kya certify writes the gap report and signed proof file`, "");
|
|
2982
|
-
}
|
|
2983
3378
|
const db = computeDashboard(model.events);
|
|
2984
3379
|
if (db.verdictMix.total > 0) {
|
|
2985
3380
|
const pct = (n) => Math.round((n / db.verdictMix.total) * 100);
|
|
2986
3381
|
// never is a reason overlay: the Never share is computed from neverEvent
|
|
2987
3382
|
// rows on top of the same 100% = total events base as the verdict buckets.
|
|
2988
|
-
|
|
3383
|
+
const om = db.outcomeMix;
|
|
3384
|
+
const mixLine = om.recorded > 0
|
|
3385
|
+
? `Outcome mix: Ran ${pct(om.ran)}% · Blocked ${pct(om.blocked)}% · Held ${pct(om.held)}%${om.error > 0 ? ` · Error ${pct(om.error)}%` : ""}${om.legacy > 0 ? ` · Earlier (no outcome): Allow ${pct(om.legacyMix.allow)}% · Review ${pct(om.legacyMix.review)}% · Deny ${pct(om.legacyMix.deny)}%` : ""} · Never ${pct(db.verdictMix.never)}%`
|
|
3386
|
+
: `Verdict mix: Allow ${pct(db.verdictMix.allow)}% · Review ${pct(db.verdictMix.review)}% · Deny ${pct(db.verdictMix.deny)}% · Never ${pct(db.verdictMix.never)}%`;
|
|
3387
|
+
lines.push("## Analytics", "", mixLine, "");
|
|
2989
3388
|
if (db.activity.buckets.length > 0) {
|
|
2990
3389
|
const max = Math.max(1, ...db.activity.buckets.map((b) => b.count));
|
|
2991
3390
|
lines.push(`Activity (${db.activity.granularity === "hour" ? "hourly" : "daily"}):`);
|
|
@@ -3013,20 +3412,8 @@ export function renderReceiptMarkdown(model) {
|
|
|
3013
3412
|
lines.push("");
|
|
3014
3413
|
}
|
|
3015
3414
|
}
|
|
3016
|
-
|
|
3017
|
-
.
|
|
3018
|
-
.flatMap((e) => {
|
|
3019
|
-
const sum = e.summary?.trim() ? ` - ${mdText(e.summary.trim())}` : "";
|
|
3020
|
-
const head = `- **${mdText(verdictWord(e.verdict))}** \`${mdInline(e.toolId)}\`${sum} - ${productLabel(e.product)} - ${mdText(e.reasonCode)}`;
|
|
3021
|
-
if (!e.diffPreview?.trim())
|
|
3022
|
-
return [head];
|
|
3023
|
-
// Fenced content is verbatim (stripEscapes only - backslash escapes
|
|
3024
|
-
// are literal in code fences); the dynamically sized tilde fence
|
|
3025
|
-
// prevents breakout and backticks cannot close a tilde fence.
|
|
3026
|
-
const diff = stripEscapes(e.diffPreview.trim());
|
|
3027
|
-
const fence = mdFence(diff);
|
|
3028
|
-
return [head, "", fence, diff, fence, ""];
|
|
3029
|
-
}), "");
|
|
3415
|
+
if (o.recorded === 0)
|
|
3416
|
+
lines.push(...feedLines);
|
|
3030
3417
|
// Same normalization as the HTML path: targetPath flows into single-line
|
|
3031
3418
|
// `#### ` code-span headers, so embedded newlines must be flattened first.
|
|
3032
3419
|
const changes = buildChangesModel(normalizeTrailEvents(model.events));
|
|
@@ -3041,7 +3428,7 @@ export function renderReceiptMarkdown(model) {
|
|
|
3041
3428
|
// fence one longer than the longest run inside.
|
|
3042
3429
|
const diff = stripEscapes(e.preview);
|
|
3043
3430
|
const fence = mdFence(diff);
|
|
3044
|
-
lines.push(`- **${mdText(
|
|
3431
|
+
lines.push(`- **${mdText(rowWord(e))}** \`${mdInline(e.toolId)}\` - ${mdText(e.ts)}`, "", fence, diff, fence, "");
|
|
3045
3432
|
}
|
|
3046
3433
|
}
|
|
3047
3434
|
}
|
|
@@ -3147,6 +3534,18 @@ export function renderReceiptMarkdown(model) {
|
|
|
3147
3534
|
const ot = model.otel;
|
|
3148
3535
|
lines.push("## Export to your monitoring (OTel)", `${ot.endpoint ? `\`${mdInline(ot.endpoint)}\` · ` : ""}${ot.spansSent} sent · ${ot.spansFailed} failed${ot.lastExportAt ? ` · last export ${relativeTime(ot.lastExportAt, nowMs)}` : ""}`, "");
|
|
3149
3536
|
}
|
|
3537
|
+
// Certify last: no gap count reaches the top of the report.
|
|
3538
|
+
if (model.certify) {
|
|
3539
|
+
const c = model.certify;
|
|
3540
|
+
lines.push("## Certify", `result: ${c.result} · counts: ${c.pass} pass · ${c.gap} gap · ${c.insufficientEvidence} insufficient · ${c.attested} attested`);
|
|
3541
|
+
if (c.topGaps.length > 0) {
|
|
3542
|
+
lines.push("Top gaps:");
|
|
3543
|
+
for (const g of c.topGaps) {
|
|
3544
|
+
lines.push(`- \`${mdInline(g.id)}\` - ${mdText(g.title)} (${mdText(g.severity)})`);
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
lines.push(`checked live - last ${c.windowDays} days, ${c.trailEvents} recorded events · the Certify tab lists every rule · kya certify writes the gap report and signed proof file`, "");
|
|
3548
|
+
}
|
|
3150
3549
|
const md = `${lines.join("\n")}\n`;
|
|
3151
3550
|
assertNoSecrets(md);
|
|
3152
3551
|
return md;
|