@solongate/proxy 0.61.0 → 0.63.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/dist/index.js +685 -296
- package/dist/tui/index.js +668 -280
- package/dist/tui/panels/Live.d.ts +4 -0
- package/package.json +1 -1
- package/dist/tui/panels/Agents.d.ts +0 -3
package/dist/tui/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { render } from "ink";
|
|
|
9
9
|
|
|
10
10
|
// src/tui/App.tsx
|
|
11
11
|
import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
|
|
12
|
-
import { useState as
|
|
12
|
+
import { useState as useState7 } from "react";
|
|
13
13
|
|
|
14
14
|
// src/cli-utils.ts
|
|
15
15
|
var c = {
|
|
@@ -124,9 +124,9 @@ function KeyHints({ hints }) {
|
|
|
124
124
|
] }, i)) });
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
// src/tui/panels/
|
|
128
|
-
import { Box as Box2, Text as Text2
|
|
129
|
-
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
127
|
+
// src/tui/panels/Live.tsx
|
|
128
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
129
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
130
130
|
|
|
131
131
|
// src/api-client/client.ts
|
|
132
132
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -429,45 +429,244 @@ function usePoll(reload, intervalMs, enabled = true) {
|
|
|
429
429
|
}, [reload, intervalMs, enabled]);
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
-
// src/tui/panels/
|
|
432
|
+
// src/tui/panels/Live.tsx
|
|
433
433
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
434
|
+
var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
435
|
+
var hhmmss = (ts) => {
|
|
436
|
+
const d = new Date(ts);
|
|
437
|
+
return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
|
|
438
|
+
};
|
|
439
|
+
var fmtUp = (ms) => {
|
|
440
|
+
const s = Math.floor(ms / 1e3);
|
|
441
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
442
|
+
return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
|
|
443
|
+
};
|
|
444
|
+
function LivePanel({ active: active2 }) {
|
|
445
|
+
const stats = useLoader(() => api.stats.get());
|
|
446
|
+
const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
|
|
447
|
+
const feed = useLoader(() => api.audit.list({ limit: 40 }));
|
|
448
|
+
usePoll(() => {
|
|
449
|
+
stats.reload();
|
|
450
|
+
ts.reload();
|
|
451
|
+
feed.reload();
|
|
452
|
+
}, 2e3, active2);
|
|
453
|
+
const [tick, setTick] = useState2(0);
|
|
454
|
+
useEffect2(() => {
|
|
455
|
+
if (!active2) return;
|
|
456
|
+
const t = setInterval(() => setTick((n) => n + 1), 200);
|
|
457
|
+
return () => clearInterval(t);
|
|
458
|
+
}, [active2]);
|
|
459
|
+
const startRef = useRef2(Date.now());
|
|
460
|
+
const [buffer, setBuffer] = useState2([]);
|
|
461
|
+
useEffect2(() => {
|
|
462
|
+
const incoming = feed.data?.entries;
|
|
463
|
+
if (!incoming?.length) return;
|
|
464
|
+
setBuffer((prev) => {
|
|
465
|
+
const seen = new Set(prev.map((e) => e.id));
|
|
466
|
+
const fresh = incoming.filter((e) => !seen.has(e.id)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
|
|
467
|
+
return fresh.length ? [...prev, ...fresh].slice(-300) : prev;
|
|
468
|
+
});
|
|
469
|
+
}, [feed.data]);
|
|
470
|
+
const s = stats.data;
|
|
471
|
+
const points = ts.data?.timeseries ?? [];
|
|
472
|
+
const cols = process.stdout.columns ?? 100;
|
|
473
|
+
const rows = process.stdout.rows ?? 32;
|
|
474
|
+
const feedRows = Math.max(8, Math.min(20, rows - 18));
|
|
475
|
+
const wide = cols >= 112;
|
|
476
|
+
const tail = buffer.slice(-feedRows);
|
|
477
|
+
const denies = buffer.filter((e) => e.decision !== "ALLOW");
|
|
478
|
+
const dlpHits = buffer.filter((e) => e.dlp_matches?.length);
|
|
479
|
+
const lastDeny = denies[denies.length - 1];
|
|
480
|
+
const spin = SPIN[tick % SPIN.length];
|
|
481
|
+
const cursor = tick % 4 < 2 ? "\u258C" : " ";
|
|
482
|
+
if (stats.error) return /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
|
|
483
|
+
"\u2717 ",
|
|
484
|
+
stats.error
|
|
485
|
+
] });
|
|
486
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
487
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
488
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "\u25CF " }),
|
|
489
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accentBright, children: "LIVE" }),
|
|
490
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
491
|
+
" ",
|
|
492
|
+
spin,
|
|
493
|
+
" scanning"
|
|
494
|
+
] }),
|
|
495
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
496
|
+
" up ",
|
|
497
|
+
fmtUp(Date.now() - startRef.current)
|
|
498
|
+
] }),
|
|
499
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
500
|
+
" ",
|
|
501
|
+
hhmmss(Date.now())
|
|
502
|
+
] }),
|
|
503
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " poll 2s" })
|
|
504
|
+
] }),
|
|
505
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
506
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "calls " }),
|
|
507
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
|
|
508
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " allow " }),
|
|
509
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
|
|
510
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " deny " }),
|
|
511
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
|
|
512
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " dlp(buf) " }),
|
|
513
|
+
/* @__PURE__ */ jsx2(Text2, { color: dlpHits.length ? theme.bad : theme.dim, children: dlpHits.length }),
|
|
514
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " policies " }),
|
|
515
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" })
|
|
516
|
+
] }),
|
|
517
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
518
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "act " }),
|
|
519
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sparkline(points.map((p) => p.total), Math.min(64, cols - 30)) })
|
|
520
|
+
] }),
|
|
521
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
522
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "dny " }),
|
|
523
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: sparkline(points.map((p) => p.denied), Math.min(64, cols - 30)) })
|
|
524
|
+
] }),
|
|
525
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
526
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ">> " }),
|
|
527
|
+
lastDeny ? /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
|
|
528
|
+
"last deny ",
|
|
529
|
+
hhmmss(lastDeny.created_at),
|
|
530
|
+
" ",
|
|
531
|
+
lastDeny.tool_name,
|
|
532
|
+
" \xB7 ",
|
|
533
|
+
truncate(lastDeny.reason ?? "policy", Math.max(20, cols - 50))
|
|
534
|
+
] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no denials in buffer" })
|
|
535
|
+
] }),
|
|
536
|
+
/* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
|
|
537
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 3, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
|
|
538
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, bold: true, children: "\u2500\u2500 tool stream \u2500\u2500" }),
|
|
539
|
+
tail.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "awaiting traffic\u2026" }) : null,
|
|
540
|
+
tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
541
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
542
|
+
"[",
|
|
543
|
+
hhmmss(e.created_at),
|
|
544
|
+
"] "
|
|
545
|
+
] }),
|
|
546
|
+
/* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
|
|
547
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool_name, 14).padEnd(15) }),
|
|
548
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
|
|
549
|
+
e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
|
|
550
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
|
|
551
|
+
] }, e.id))
|
|
552
|
+
] }),
|
|
553
|
+
wide ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: 34, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
|
|
554
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, bold: true, children: "\u2500\u2500 raw \u2500\u2500" }),
|
|
555
|
+
tail.slice(-Math.floor(feedRows / 2)).flatMap((e) => {
|
|
556
|
+
const raw = e.arguments_summary ? JSON.stringify(e.arguments_summary) : `hash:${e.id.slice(0, 12)}`;
|
|
557
|
+
return [
|
|
558
|
+
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", color: e.decision === "ALLOW" ? theme.dim : theme.bad, children: [
|
|
559
|
+
e.id.slice(0, 8),
|
|
560
|
+
" ",
|
|
561
|
+
e.tool_name.slice(0, 12)
|
|
562
|
+
] }, e.id + ":h"),
|
|
563
|
+
/* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.dim, children: " " + raw.replace(/\s+/g, " ") }, e.id + ":b")
|
|
564
|
+
];
|
|
565
|
+
})
|
|
566
|
+
] }) : null
|
|
567
|
+
] }),
|
|
568
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
569
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "root@solongate" }),
|
|
570
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ":" }),
|
|
571
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: "~/live" }),
|
|
572
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "$ " }),
|
|
573
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: cursor })
|
|
574
|
+
] })
|
|
575
|
+
] });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/tui/panels/Policies.tsx
|
|
579
|
+
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
580
|
+
import TextInput from "ink-text-input";
|
|
581
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
582
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
583
|
+
var constr = (r, g, side) => (r[g]?.[side] ?? []).join(", ");
|
|
584
|
+
var setConstr = (r, g, side, val) => {
|
|
585
|
+
const arr = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
586
|
+
const prev = r[g] ?? {};
|
|
587
|
+
return { ...r, [g]: { ...prev, [side]: arr } };
|
|
588
|
+
};
|
|
589
|
+
var FIELDS = [
|
|
590
|
+
{ label: "Effect", kind: "effect", get: (r) => r.effect },
|
|
591
|
+
{ label: "Enabled", kind: "enabled", get: (r) => r.enabled ? "yes" : "no" },
|
|
592
|
+
{ label: "Priority", kind: "priority", get: (r) => String(r.priority) },
|
|
593
|
+
{ label: "Tool pattern", kind: "text", get: (r) => r.toolPattern, set: (r, v) => ({ ...r, toolPattern: v || "*" }) },
|
|
594
|
+
{ label: "Description", kind: "text", get: (r) => r.description ?? "", set: (r, v) => ({ ...r, description: v }) },
|
|
595
|
+
{ label: "Cmd allow", kind: "text", get: (r) => constr(r, "commandConstraints", "allowed"), set: (r, v) => setConstr(r, "commandConstraints", "allowed", v) },
|
|
596
|
+
{ label: "Cmd deny", kind: "text", get: (r) => constr(r, "commandConstraints", "denied"), set: (r, v) => setConstr(r, "commandConstraints", "denied", v) },
|
|
597
|
+
{ label: "Path allow", kind: "text", get: (r) => constr(r, "pathConstraints", "allowed"), set: (r, v) => setConstr(r, "pathConstraints", "allowed", v) },
|
|
598
|
+
{ label: "Path deny", kind: "text", get: (r) => constr(r, "pathConstraints", "denied"), set: (r, v) => setConstr(r, "pathConstraints", "denied", v) },
|
|
599
|
+
{ label: "File allow", kind: "text", get: (r) => constr(r, "filenameConstraints", "allowed"), set: (r, v) => setConstr(r, "filenameConstraints", "allowed", v) },
|
|
600
|
+
{ label: "File deny", kind: "text", get: (r) => constr(r, "filenameConstraints", "denied"), set: (r, v) => setConstr(r, "filenameConstraints", "denied", v) },
|
|
601
|
+
{ label: "URL allow", kind: "text", get: (r) => constr(r, "urlConstraints", "allowed"), set: (r, v) => setConstr(r, "urlConstraints", "allowed", v) },
|
|
602
|
+
{ label: "URL deny", kind: "text", get: (r) => constr(r, "urlConstraints", "denied"), set: (r, v) => setConstr(r, "urlConstraints", "denied", v) }
|
|
603
|
+
];
|
|
604
|
+
function ruleSummary(r) {
|
|
605
|
+
const bits = [];
|
|
606
|
+
for (const [g, tag] of [["commandConstraints", "cmd"], ["pathConstraints", "path"], ["filenameConstraints", "file"], ["urlConstraints", "url"]]) {
|
|
607
|
+
const c2 = r[g];
|
|
608
|
+
const n = (c2?.allowed?.length ?? 0) + (c2?.denied?.length ?? 0);
|
|
609
|
+
if (n) bits.push(`${tag}:${n}`);
|
|
610
|
+
}
|
|
611
|
+
return bits.join(" ");
|
|
612
|
+
}
|
|
434
613
|
function PoliciesPanel({ focused }) {
|
|
435
614
|
const list3 = useLoader(() => api.policies.list());
|
|
436
615
|
const policies = list3.data?.policies ?? [];
|
|
437
|
-
const [view, setView] =
|
|
438
|
-
const [pi, setPi] =
|
|
439
|
-
const [ri, setRi] =
|
|
440
|
-
const [
|
|
441
|
-
const [
|
|
442
|
-
const [
|
|
616
|
+
const [view, setView] = useState3("list");
|
|
617
|
+
const [pi, setPi] = useState3(0);
|
|
618
|
+
const [ri, setRi] = useState3(0);
|
|
619
|
+
const [fi, setFi] = useState3(0);
|
|
620
|
+
const [rules, setRules] = useState3([]);
|
|
621
|
+
const [mode, setMode] = useState3("denylist");
|
|
622
|
+
const [dirty, setDirty] = useState3(false);
|
|
623
|
+
const [editing, setEditing] = useState3(false);
|
|
624
|
+
const [editVal, setEditVal] = useState3("");
|
|
625
|
+
const [status, setStatus] = useState3(null);
|
|
443
626
|
const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
|
|
444
627
|
const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
|
|
445
|
-
|
|
628
|
+
useEffect3(() => {
|
|
446
629
|
if (detail.data) {
|
|
447
630
|
setRules(detail.data.rules);
|
|
448
631
|
setMode(detail.data.mode ?? "denylist");
|
|
632
|
+
setDirty(false);
|
|
449
633
|
setRi(0);
|
|
450
634
|
}
|
|
451
635
|
}, [detail.data]);
|
|
452
|
-
const
|
|
453
|
-
|
|
636
|
+
const mutate = (nextRules, nextMode = mode) => {
|
|
637
|
+
setRules(nextRules);
|
|
638
|
+
setMode(nextMode);
|
|
639
|
+
setDirty(true);
|
|
640
|
+
setStatus(null);
|
|
641
|
+
};
|
|
642
|
+
const save = async () => {
|
|
643
|
+
if (!selected || !detail.data) return;
|
|
454
644
|
setStatus("Saving\u2026");
|
|
455
645
|
try {
|
|
456
646
|
const res = await api.policies.update(selected.id, {
|
|
457
647
|
id: selected.id,
|
|
458
648
|
name: detail.data.name,
|
|
459
649
|
description: detail.data.description,
|
|
460
|
-
mode
|
|
650
|
+
mode,
|
|
461
651
|
agents: detail.data.agents,
|
|
462
|
-
rules
|
|
652
|
+
rules
|
|
463
653
|
});
|
|
464
654
|
setRules(res.rules);
|
|
655
|
+
setDirty(false);
|
|
465
656
|
setStatus("\u2713 Saved v" + res._version);
|
|
466
657
|
list3.reload();
|
|
467
658
|
} catch (e) {
|
|
468
659
|
setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
|
|
469
660
|
}
|
|
470
661
|
};
|
|
662
|
+
const discard = () => {
|
|
663
|
+
if (detail.data) {
|
|
664
|
+
setRules(detail.data.rules);
|
|
665
|
+
setMode(detail.data.mode ?? "denylist");
|
|
666
|
+
}
|
|
667
|
+
setDirty(false);
|
|
668
|
+
setStatus("discarded");
|
|
669
|
+
};
|
|
471
670
|
useInput(
|
|
472
671
|
(input, key) => {
|
|
473
672
|
if (view === "list") {
|
|
@@ -475,44 +674,61 @@ function PoliciesPanel({ focused }) {
|
|
|
475
674
|
else if (key.downArrow) setPi((n) => Math.min(policies.length - 1, n + 1));
|
|
476
675
|
else if (key.return || key.rightArrow) {
|
|
477
676
|
setStatus(null);
|
|
478
|
-
setView("
|
|
677
|
+
setView("rules");
|
|
479
678
|
}
|
|
480
679
|
return;
|
|
481
680
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
681
|
+
if (view === "rules") {
|
|
682
|
+
if (key.leftArrow) return setView("list"), void setStatus(null);
|
|
683
|
+
if (key.upArrow) setRi((n) => Math.max(0, n - 1));
|
|
684
|
+
else if (key.downArrow) setRi((n) => Math.min(rules.length - 1, n + 1));
|
|
685
|
+
else if (key.return || key.rightArrow) {
|
|
686
|
+
if (rules[ri]) {
|
|
687
|
+
setFi(0);
|
|
688
|
+
setView("rule");
|
|
689
|
+
}
|
|
690
|
+
} else if (input === " ") {
|
|
691
|
+
if (rules[ri]) mutate(rules.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r));
|
|
692
|
+
} else if (input === "e") {
|
|
693
|
+
if (rules[ri]) mutate(rules.map((r, i) => i === ri ? { ...r, effect: r.effect === "ALLOW" ? "DENY" : "ALLOW" } : r));
|
|
694
|
+
} else if (input === "d") {
|
|
695
|
+
if (rules[ri]) {
|
|
696
|
+
const next = rules.filter((_, i) => i !== ri);
|
|
697
|
+
setRi((n) => Math.max(0, Math.min(n, next.length - 1)));
|
|
698
|
+
mutate(next);
|
|
699
|
+
}
|
|
700
|
+
} else if (input === "m") {
|
|
701
|
+
mutate(rules, mode === "denylist" ? "whitelist" : "denylist");
|
|
702
|
+
} else if (input === "s") void save();
|
|
703
|
+
else if (input === "x") discard();
|
|
704
|
+
return;
|
|
506
705
|
}
|
|
706
|
+
const rule2 = rules[ri];
|
|
707
|
+
if (!rule2) return setView("rules");
|
|
708
|
+
const field = FIELDS[fi];
|
|
709
|
+
if (key.leftArrow) setView("rules");
|
|
710
|
+
else if (key.upArrow) setFi((n) => Math.max(0, n - 1));
|
|
711
|
+
else if (key.downArrow) setFi((n) => Math.min(FIELDS.length - 1, n + 1));
|
|
712
|
+
else if (field.kind === "effect" && (input === " " || key.rightArrow)) {
|
|
713
|
+
mutate(rules.map((r, i) => i === ri ? { ...r, effect: r.effect === "ALLOW" ? "DENY" : "ALLOW" } : r));
|
|
714
|
+
} else if (field.kind === "enabled" && (input === " " || key.rightArrow)) {
|
|
715
|
+
mutate(rules.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r));
|
|
716
|
+
} else if (field.kind === "priority" && (key.rightArrow || key.leftArrow)) {
|
|
717
|
+
const d = key.rightArrow ? 1 : -1;
|
|
718
|
+
mutate(rules.map((r, i) => i === ri ? { ...r, priority: Math.max(0, r.priority + d) } : r));
|
|
719
|
+
} else if (field.kind === "text" && key.return) {
|
|
720
|
+
setEditVal(field.get(rule2));
|
|
721
|
+
setEditing(true);
|
|
722
|
+
} else if (input === "s") void save();
|
|
507
723
|
},
|
|
508
|
-
{ isActive: focused }
|
|
724
|
+
{ isActive: focused && !editing }
|
|
509
725
|
);
|
|
510
726
|
if (view === "list") {
|
|
511
|
-
return /* @__PURE__ */
|
|
512
|
-
/* @__PURE__ */
|
|
513
|
-
/* @__PURE__ */
|
|
727
|
+
return /* @__PURE__ */ jsx3(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
728
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
|
|
729
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs3(Text3, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
|
|
514
730
|
(i === pi ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
|
|
515
|
-
/* @__PURE__ */
|
|
731
|
+
/* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
|
|
516
732
|
p.mode.padEnd(10),
|
|
517
733
|
" ",
|
|
518
734
|
p.rules.length,
|
|
@@ -522,58 +738,108 @@ function PoliciesPanel({ focused }) {
|
|
|
522
738
|
] }, p.id)) })
|
|
523
739
|
] }) });
|
|
524
740
|
}
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
/* @__PURE__ */
|
|
528
|
-
/* @__PURE__ */
|
|
529
|
-
|
|
530
|
-
|
|
741
|
+
const dirtyTag = dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null;
|
|
742
|
+
if (view === "rules") {
|
|
743
|
+
return /* @__PURE__ */ jsx3(DataView, { loading: detail.loading && !detail.data, error: detail.error, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
744
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
745
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 28) }),
|
|
746
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " mode: " }),
|
|
747
|
+
/* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
|
|
748
|
+
dirtyTag
|
|
749
|
+
] }),
|
|
750
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 rule \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 d delete \xB7 m mode \xB7 s save \xB7 \u2190 back" }),
|
|
751
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
|
|
752
|
+
Table,
|
|
753
|
+
{
|
|
754
|
+
columns: [
|
|
755
|
+
{ header: "", width: 2 },
|
|
756
|
+
{ header: "ON", width: 2 },
|
|
757
|
+
{ header: "EFFECT", width: 7 },
|
|
758
|
+
{ header: "PRIO", width: 4 },
|
|
759
|
+
{ header: "TOOL", width: 20 },
|
|
760
|
+
{ header: "CONSTRAINTS", width: 22 }
|
|
761
|
+
],
|
|
762
|
+
rows: rules.map((r, i) => [
|
|
763
|
+
{ value: i === ri ? "\u25B8" : "", color: theme.accentBright },
|
|
764
|
+
{ value: r.enabled ? "\u25CF" : "\u25CB", color: r.enabled ? theme.ok : theme.dim },
|
|
765
|
+
{ value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
|
|
766
|
+
{ value: String(r.priority), dim: true },
|
|
767
|
+
{ value: truncate(r.toolPattern, 20), color: theme.accent, dim: !r.enabled },
|
|
768
|
+
{ value: ruleSummary(r) || truncate(r.description || "\u2014", 22), dim: true }
|
|
769
|
+
])
|
|
770
|
+
}
|
|
771
|
+
) }),
|
|
772
|
+
rules.length === 0 ? /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "(no rules)" }) : null,
|
|
773
|
+
status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
|
|
774
|
+
] }) });
|
|
775
|
+
}
|
|
776
|
+
const rule = rules[ri];
|
|
777
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
778
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
779
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: "Rule " }),
|
|
780
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: truncate(rule?.id ?? "", 26) }),
|
|
781
|
+
dirtyTag
|
|
531
782
|
] }),
|
|
532
|
-
/* @__PURE__ */
|
|
533
|
-
/* @__PURE__ */
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
{
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
783
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 field \xB7 enter edit text \xB7 space/\u2192 toggle \xB7 s save \xB7 \u2190 back" }),
|
|
784
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: FIELDS.map((f, i) => {
|
|
785
|
+
const val = rule ? f.get(rule) : "";
|
|
786
|
+
const active2 = i === fi;
|
|
787
|
+
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
788
|
+
/* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + f.label.padEnd(13) }),
|
|
789
|
+
active2 && editing && f.kind === "text" ? /* @__PURE__ */ jsx3(
|
|
790
|
+
TextInput,
|
|
791
|
+
{
|
|
792
|
+
value: editVal,
|
|
793
|
+
onChange: setEditVal,
|
|
794
|
+
onSubmit: (v) => {
|
|
795
|
+
if (rule && f.set) mutate(rules.map((r, idx) => idx === ri ? f.set(r, v) : r));
|
|
796
|
+
setEditing(false);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
) : /* @__PURE__ */ jsx3(
|
|
800
|
+
Text3,
|
|
801
|
+
{
|
|
802
|
+
color: f.kind === "effect" ? val === "ALLOW" ? theme.ok : theme.bad : void 0,
|
|
803
|
+
dimColor: !val,
|
|
804
|
+
children: val || "\u2014"
|
|
805
|
+
}
|
|
806
|
+
)
|
|
807
|
+
] }, f.label);
|
|
808
|
+
}) }),
|
|
809
|
+
status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
|
|
810
|
+
] });
|
|
555
811
|
}
|
|
556
812
|
|
|
557
813
|
// src/tui/panels/RateLimit.tsx
|
|
558
|
-
import { Box as
|
|
559
|
-
import { useEffect as
|
|
560
|
-
import { jsx as
|
|
814
|
+
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
815
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
816
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
561
817
|
var MODES = ["off", "detect", "block"];
|
|
562
|
-
var
|
|
818
|
+
var FIELDS2 = ["mode", "perMinute", "perHour", "perDay"];
|
|
819
|
+
var BLOCKS2 = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
820
|
+
function ColoredSpark({ values, limit, width = 60 }) {
|
|
821
|
+
const v = values.length > width ? values.slice(values.length - width) : values;
|
|
822
|
+
const max = Math.max(1, ...v);
|
|
823
|
+
return /* @__PURE__ */ jsx4(Text4, { children: v.map((n, i) => {
|
|
824
|
+
const ch = BLOCKS2[Math.min(BLOCKS2.length - 1, Math.round(n / max * (BLOCKS2.length - 1)))];
|
|
825
|
+
const over = limit > 0 && n > limit;
|
|
826
|
+
return /* @__PURE__ */ jsx4(Text4, { color: over ? theme.bad : theme.accent, children: ch }, i);
|
|
827
|
+
}) });
|
|
828
|
+
}
|
|
563
829
|
function RateLimitPanel({ focused }) {
|
|
564
830
|
const layersQ = useLoader(() => api.settings.getSecurityLayers());
|
|
565
831
|
const historyQ = useLoader(() => api.settings.getRateLimitHistory());
|
|
566
832
|
const insightsQ = useLoader(() => api.stats.securityInsights(7));
|
|
567
|
-
const [draft, setDraft] =
|
|
568
|
-
const [dirty, setDirty] =
|
|
569
|
-
const [fi, setFi] =
|
|
570
|
-
const [status, setStatus] =
|
|
571
|
-
|
|
833
|
+
const [draft, setDraft] = useState4(null);
|
|
834
|
+
const [dirty, setDirty] = useState4(false);
|
|
835
|
+
const [fi, setFi] = useState4(0);
|
|
836
|
+
const [status, setStatus] = useState4(null);
|
|
837
|
+
useEffect4(() => {
|
|
572
838
|
if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
|
|
573
839
|
}, [layersQ.data, draft]);
|
|
574
840
|
const adjust = (dir, step) => {
|
|
575
841
|
if (!draft) return;
|
|
576
|
-
const field =
|
|
842
|
+
const field = FIELDS2[fi];
|
|
577
843
|
if (field === "mode") {
|
|
578
844
|
const idx = (MODES.indexOf(draft.mode) + dir + MODES.length) % MODES.length;
|
|
579
845
|
setDraft({ ...draft, mode: MODES[idx] });
|
|
@@ -587,8 +853,7 @@ function RateLimitPanel({ focused }) {
|
|
|
587
853
|
if (!draft || !layersQ.data) return;
|
|
588
854
|
setStatus("Saving\u2026");
|
|
589
855
|
try {
|
|
590
|
-
const
|
|
591
|
-
const res = await api.settings.setSecurityLayers(next);
|
|
856
|
+
const res = await api.settings.setSecurityLayers({ ...layersQ.data.layers, rateLimit: draft });
|
|
592
857
|
setDraft({ ...res.layers.rateLimit });
|
|
593
858
|
setDirty(false);
|
|
594
859
|
setStatus("\u2713 Saved");
|
|
@@ -600,8 +865,8 @@ function RateLimitPanel({ focused }) {
|
|
|
600
865
|
useInput2(
|
|
601
866
|
(input, key) => {
|
|
602
867
|
const step = key.shift ? 10 : 1;
|
|
603
|
-
if (key.upArrow) setFi((n) => (n - 1 +
|
|
604
|
-
else if (key.downArrow) setFi((n) => (n + 1) %
|
|
868
|
+
if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
|
|
869
|
+
else if (key.downArrow) setFi((n) => (n + 1) % FIELDS2.length);
|
|
605
870
|
else if (key.leftArrow) adjust(-1, step);
|
|
606
871
|
else if (key.rightArrow) adjust(1, step);
|
|
607
872
|
else if (input === "s") void save();
|
|
@@ -609,155 +874,206 @@ function RateLimitPanel({ focused }) {
|
|
|
609
874
|
{ isActive: focused }
|
|
610
875
|
);
|
|
611
876
|
const history = (historyQ.data?.history ?? []).slice().sort((a, b) => b.ts - a.ts);
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
/* @__PURE__ */
|
|
877
|
+
const activity = insightsQ.data?.["activity"] ?? {};
|
|
878
|
+
const minuteSeries = (activity.minute ?? []).map((b) => b.count);
|
|
879
|
+
const hourSeries = (activity.hour ?? []).map((b) => b.count);
|
|
880
|
+
const bursts = minuteSeries.filter((c2) => draft && draft.perMinute > 0 && c2 > draft.perMinute).length;
|
|
881
|
+
return /* @__PURE__ */ jsx4(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
882
|
+
/* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: focused ? "\u2191\u2193 field \xB7 \u2190\u2192 \xB11 \xB7 shift+\u2190\u2192 \xB110 \xB7 s save" : "press \u2192 to edit" }),
|
|
883
|
+
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
|
|
884
|
+
/* @__PURE__ */ jsx4(FieldRow, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx4(Text4, { color: modeColor(draft.mode), children: draft.mode }) }),
|
|
885
|
+
/* @__PURE__ */ jsx4(FieldRow, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perMinute }) }),
|
|
886
|
+
/* @__PURE__ */ jsx4(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
|
|
887
|
+
/* @__PURE__ */ jsx4(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
|
|
620
888
|
] }),
|
|
621
|
-
dirty || status ? /* @__PURE__ */
|
|
622
|
-
dirty ? /* @__PURE__ */
|
|
623
|
-
status ? /* @__PURE__ */
|
|
889
|
+
dirty || status ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
|
|
890
|
+
dirty ? /* @__PURE__ */ jsx4(Text4, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
|
|
891
|
+
status ? /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
|
|
624
892
|
] }) : null,
|
|
625
|
-
/* @__PURE__ */
|
|
626
|
-
/* @__PURE__ */
|
|
893
|
+
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
|
|
894
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
895
|
+
/* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "Activity " }),
|
|
896
|
+
bursts > 0 ? /* @__PURE__ */ jsxs4(Text4, { color: theme.bad, children: [
|
|
897
|
+
bursts,
|
|
898
|
+
" burst min"
|
|
899
|
+
] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no bursts" }),
|
|
900
|
+
insightsQ.loading && !insightsQ.data ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " \u2026" }) : null
|
|
901
|
+
] }),
|
|
902
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
903
|
+
/* @__PURE__ */ jsx4(Text4, { children: "/min " }),
|
|
904
|
+
minuteSeries.length ? /* @__PURE__ */ jsx4(ColoredSpark, { values: minuteSeries, limit: draft.perMinute }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no data" })
|
|
905
|
+
] }),
|
|
906
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
907
|
+
/* @__PURE__ */ jsx4(Text4, { children: "/hour " }),
|
|
908
|
+
hourSeries.length ? /* @__PURE__ */ jsx4(ColoredSpark, { values: hourSeries, limit: draft.perHour, width: 24 }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no data" })
|
|
909
|
+
] })
|
|
910
|
+
] }),
|
|
911
|
+
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
|
|
912
|
+
/* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
|
|
627
913
|
"Change history ",
|
|
628
914
|
history.length ? `(${history.length})` : ""
|
|
629
915
|
] }),
|
|
630
|
-
history.length ? /* @__PURE__ */
|
|
916
|
+
history.length ? /* @__PURE__ */ jsx4(
|
|
631
917
|
Table,
|
|
632
918
|
{
|
|
633
919
|
columns: [
|
|
634
|
-
{ header: "WHEN", width:
|
|
920
|
+
{ header: "WHEN", width: 10 },
|
|
635
921
|
{ header: "/MIN", width: 6 },
|
|
636
922
|
{ header: "/HOUR", width: 7 },
|
|
637
923
|
{ header: "/DAY", width: 6 }
|
|
638
924
|
],
|
|
639
|
-
rows: history.slice(0,
|
|
925
|
+
rows: history.slice(0, 4).map((h, i) => [
|
|
640
926
|
{ value: ago(h.ts) + " ago", dim: true, bold: i === 0 },
|
|
641
927
|
{ value: String(h.minute), bold: i === 0 },
|
|
642
928
|
{ value: h.hour === 0 ? "off" : String(h.hour), dim: h.hour === 0 },
|
|
643
929
|
{ value: h.day === 0 ? "off" : String(h.day), dim: h.day === 0 }
|
|
644
930
|
])
|
|
645
931
|
}
|
|
646
|
-
) : /* @__PURE__ */
|
|
647
|
-
] }),
|
|
648
|
-
/* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
|
|
649
|
-
/* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
|
|
650
|
-
"Recent bursts ",
|
|
651
|
-
insightsQ.loading && !insightsQ.data ? "\u2026" : `(${bursts.length})`
|
|
652
|
-
] }),
|
|
653
|
-
bursts.length ? /* @__PURE__ */ jsx3(
|
|
654
|
-
Table,
|
|
655
|
-
{
|
|
656
|
-
columns: [
|
|
657
|
-
{ header: "RESULT", width: 9 },
|
|
658
|
-
{ header: "AGENT", width: 16 },
|
|
659
|
-
{ header: "CALLS/LIMIT", width: 12 },
|
|
660
|
-
{ header: "WHEN", width: 8 }
|
|
661
|
-
],
|
|
662
|
-
rows: bursts.map((b) => [
|
|
663
|
-
{ value: b.blocked ? "blocked" : "BYPASSED", color: b.blocked ? theme.accent : theme.bad, bold: !b.blocked },
|
|
664
|
-
{ value: (b.agent || "\u2014").slice(0, 16), dim: true },
|
|
665
|
-
{ value: `${b.count}/${b.limit}` },
|
|
666
|
-
{ value: ago(b.minute) + " ago", dim: true }
|
|
667
|
-
])
|
|
668
|
-
}
|
|
669
|
-
) : /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
|
|
670
|
-
" ",
|
|
671
|
-
insightsQ.error ? insightsQ.error : "none in 7d"
|
|
672
|
-
] })
|
|
932
|
+
) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " no changes" })
|
|
673
933
|
] })
|
|
674
934
|
] }) : null });
|
|
675
935
|
}
|
|
676
936
|
function FieldRow({ label, active: active2, children }) {
|
|
677
|
-
return /* @__PURE__ */
|
|
678
|
-
/* @__PURE__ */
|
|
937
|
+
return /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
938
|
+
/* @__PURE__ */ jsx4(Text4, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
|
|
679
939
|
children
|
|
680
940
|
] });
|
|
681
941
|
}
|
|
682
942
|
|
|
683
943
|
// src/tui/panels/Dlp.tsx
|
|
684
|
-
import { Box as
|
|
685
|
-
import
|
|
686
|
-
import {
|
|
944
|
+
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
945
|
+
import TextInput2 from "ink-text-input";
|
|
946
|
+
import { useEffect as useEffect5, useState as useState5 } from "react";
|
|
947
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
687
948
|
var MODES2 = ["off", "detect", "block"];
|
|
688
949
|
function DlpPanel({ focused }) {
|
|
689
950
|
const q = useLoader(() => api.settings.getSecurityLayers());
|
|
690
|
-
const [dlp, setDlp] =
|
|
691
|
-
const [available, setAvailable] =
|
|
692
|
-
const [sel, setSel] =
|
|
693
|
-
const [
|
|
694
|
-
|
|
951
|
+
const [dlp, setDlp] = useState5(null);
|
|
952
|
+
const [available, setAvailable] = useState5([]);
|
|
953
|
+
const [sel, setSel] = useState5(0);
|
|
954
|
+
const [dirty, setDirty] = useState5(false);
|
|
955
|
+
const [status, setStatus] = useState5(null);
|
|
956
|
+
const [adding, setAdding] = useState5(null);
|
|
957
|
+
const [newName, setNewName] = useState5("");
|
|
958
|
+
const [newRe, setNewRe] = useState5("");
|
|
959
|
+
useEffect5(() => {
|
|
695
960
|
if (q.data && !dlp) {
|
|
696
|
-
setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns] });
|
|
961
|
+
setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
|
|
697
962
|
setAvailable(q.data.availablePatterns);
|
|
698
963
|
}
|
|
699
964
|
}, [q.data, dlp]);
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
965
|
+
const mutate = (next) => {
|
|
966
|
+
setDlp(next);
|
|
967
|
+
setDirty(true);
|
|
968
|
+
setStatus(null);
|
|
969
|
+
};
|
|
970
|
+
const save = async () => {
|
|
971
|
+
if (!dlp || !q.data) return;
|
|
703
972
|
setStatus("Saving\u2026");
|
|
704
973
|
try {
|
|
705
|
-
const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp
|
|
706
|
-
setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns] });
|
|
974
|
+
const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp });
|
|
975
|
+
setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns], custom: [...res.layers.dlp.custom] });
|
|
976
|
+
setDirty(false);
|
|
707
977
|
setStatus("\u2713 Saved");
|
|
708
978
|
} catch (e) {
|
|
709
979
|
setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
|
|
710
980
|
}
|
|
711
981
|
};
|
|
982
|
+
const discard = () => {
|
|
983
|
+
if (q.data) setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
|
|
984
|
+
setDirty(false);
|
|
985
|
+
setStatus("discarded");
|
|
986
|
+
};
|
|
987
|
+
const customCount = dlp?.custom.length ?? 0;
|
|
988
|
+
const total = available.length + customCount;
|
|
712
989
|
useInput3(
|
|
713
990
|
(input, key) => {
|
|
714
991
|
if (!dlp) return;
|
|
715
992
|
if (key.upArrow) setSel((n) => Math.max(0, n - 1));
|
|
716
|
-
else if (key.downArrow) setSel((n) => Math.min(
|
|
717
|
-
else if (input === "
|
|
993
|
+
else if (key.downArrow) setSel((n) => Math.min(total - 1, n + 1));
|
|
994
|
+
else if (input === "m") {
|
|
995
|
+
const idx = (MODES2.indexOf(dlp.mode) + 1) % MODES2.length;
|
|
996
|
+
mutate({ ...dlp, mode: MODES2[idx] });
|
|
997
|
+
} else if (input === "a") {
|
|
998
|
+
setNewName("");
|
|
999
|
+
setNewRe("");
|
|
1000
|
+
setAdding("name");
|
|
1001
|
+
} else if (input === " " && sel < available.length) {
|
|
718
1002
|
const p = available[sel];
|
|
719
|
-
if (!p) return;
|
|
720
1003
|
const set = new Set(dlp.patterns);
|
|
721
1004
|
if (set.has(p)) set.delete(p);
|
|
722
1005
|
else set.add(p);
|
|
723
|
-
|
|
724
|
-
} else if (input === "
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
1006
|
+
mutate({ ...dlp, patterns: [...set] });
|
|
1007
|
+
} else if (input === "d" && sel >= available.length) {
|
|
1008
|
+
const ci = sel - available.length;
|
|
1009
|
+
mutate({ ...dlp, custom: dlp.custom.filter((_, i) => i !== ci) });
|
|
1010
|
+
setSel((n) => Math.max(0, n - 1));
|
|
1011
|
+
} else if (input === "s") void save();
|
|
1012
|
+
else if (input === "x") discard();
|
|
728
1013
|
},
|
|
729
|
-
{ isActive: focused }
|
|
1014
|
+
{ isActive: focused && !adding }
|
|
730
1015
|
);
|
|
731
1016
|
const enabled = new Set(dlp?.patterns ?? []);
|
|
732
|
-
return /* @__PURE__ */
|
|
733
|
-
/* @__PURE__ */
|
|
734
|
-
/* @__PURE__ */
|
|
735
|
-
/* @__PURE__ */
|
|
736
|
-
/* @__PURE__ */
|
|
1017
|
+
return /* @__PURE__ */ jsx5(DataView, { loading: q.loading && !dlp, error: q.error, children: dlp ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
1018
|
+
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1019
|
+
/* @__PURE__ */ jsx5(Text5, { children: "mode: " }),
|
|
1020
|
+
/* @__PURE__ */ jsx5(Text5, { color: modeColor(dlp.mode), children: dlp.mode }),
|
|
1021
|
+
dirty ? /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null
|
|
737
1022
|
] }),
|
|
738
|
-
/* @__PURE__ */
|
|
1023
|
+
/* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 s save" : "press \u2192 to edit" }),
|
|
1024
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
|
|
739
1025
|
const on = enabled.has(p);
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1026
|
+
const active2 = focused && sel === i;
|
|
1027
|
+
return /* @__PURE__ */ jsxs5(Text5, { color: active2 ? theme.accentBright : void 0, bold: active2, children: [
|
|
1028
|
+
(active2 ? "\u25B8 " : " ") + (on ? "\u25CF " : "\u25CB "),
|
|
1029
|
+
/* @__PURE__ */ jsx5(Text5, { color: on ? theme.ok : theme.dim, children: p })
|
|
743
1030
|
] }, p);
|
|
744
1031
|
}) }),
|
|
745
|
-
|
|
746
|
-
/* @__PURE__ */
|
|
747
|
-
dlp.custom.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1032
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1033
|
+
/* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "Custom patterns" }),
|
|
1034
|
+
dlp.custom.length === 0 ? /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " (none \u2014 press a to add)" }) : null,
|
|
1035
|
+
dlp.custom.map((c2, i) => {
|
|
1036
|
+
const active2 = focused && sel === available.length + i;
|
|
1037
|
+
return /* @__PURE__ */ jsxs5(Text5, { color: active2 ? theme.accentBright : void 0, bold: active2, children: [
|
|
1038
|
+
active2 ? "\u25B8 " : " ",
|
|
1039
|
+
/* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: c2.name }),
|
|
1040
|
+
" ",
|
|
1041
|
+
/* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: c2.re })
|
|
1042
|
+
] }, c2.name + i);
|
|
1043
|
+
})
|
|
1044
|
+
] }),
|
|
1045
|
+
adding ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
|
|
1046
|
+
/* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: adding === "name" ? "New pattern name: " : `Regex for "${newName}": ` }),
|
|
1047
|
+
adding === "name" ? /* @__PURE__ */ jsx5(
|
|
1048
|
+
TextInput2,
|
|
1049
|
+
{
|
|
1050
|
+
value: newName,
|
|
1051
|
+
onChange: setNewName,
|
|
1052
|
+
onSubmit: (v) => {
|
|
1053
|
+
if (!v.trim()) return setAdding(null);
|
|
1054
|
+
setNewName(v.trim());
|
|
1055
|
+
setAdding("re");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
) : /* @__PURE__ */ jsx5(
|
|
1059
|
+
TextInput2,
|
|
1060
|
+
{
|
|
1061
|
+
value: newRe,
|
|
1062
|
+
onChange: setNewRe,
|
|
1063
|
+
onSubmit: (v) => {
|
|
1064
|
+
if (v.trim() && dlp) mutate({ ...dlp, custom: [...dlp.custom, { name: newName, re: v.trim() }] });
|
|
1065
|
+
setAdding(null);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
)
|
|
753
1069
|
] }) : null,
|
|
754
|
-
status ? /* @__PURE__ */
|
|
1070
|
+
status ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
|
|
755
1071
|
] }) : null });
|
|
756
1072
|
}
|
|
757
1073
|
|
|
758
1074
|
// src/tui/panels/Stats.tsx
|
|
759
|
-
import { Box as
|
|
760
|
-
import { jsx as
|
|
1075
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
1076
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
761
1077
|
function StatsPanel({ active: active2 }) {
|
|
762
1078
|
const stats = useLoader(() => api.stats.get());
|
|
763
1079
|
const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
|
|
@@ -767,50 +1083,50 @@ function StatsPanel({ active: active2 }) {
|
|
|
767
1083
|
}, 5e3, active2);
|
|
768
1084
|
const s = stats.data;
|
|
769
1085
|
const points = ts.data?.timeseries ?? [];
|
|
770
|
-
return /* @__PURE__ */
|
|
771
|
-
/* @__PURE__ */
|
|
772
|
-
/* @__PURE__ */
|
|
1086
|
+
return /* @__PURE__ */ jsx6(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
1087
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
1088
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
|
|
773
1089
|
s.total_calls,
|
|
774
1090
|
" "
|
|
775
1091
|
] }),
|
|
776
|
-
/* @__PURE__ */
|
|
777
|
-
/* @__PURE__ */
|
|
1092
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "calls " }),
|
|
1093
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.ok, children: [
|
|
778
1094
|
s.allowed,
|
|
779
1095
|
" allowed"
|
|
780
1096
|
] }),
|
|
781
|
-
/* @__PURE__ */
|
|
782
|
-
/* @__PURE__ */
|
|
1097
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: " " }),
|
|
1098
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.bad, children: [
|
|
783
1099
|
s.denied,
|
|
784
1100
|
" denied"
|
|
785
1101
|
] })
|
|
786
1102
|
] }),
|
|
787
|
-
/* @__PURE__ */
|
|
1103
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
|
|
788
1104
|
s.active_policies,
|
|
789
1105
|
" policies \xB7 ",
|
|
790
1106
|
s.registered_tools,
|
|
791
1107
|
" tools"
|
|
792
1108
|
] }),
|
|
793
|
-
/* @__PURE__ */
|
|
794
|
-
/* @__PURE__ */
|
|
1109
|
+
/* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
1110
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
|
|
795
1111
|
"Last 24h ",
|
|
796
1112
|
ts.data ? `(per ${ts.data.granularity})` : ""
|
|
797
1113
|
] }),
|
|
798
|
-
/* @__PURE__ */
|
|
799
|
-
/* @__PURE__ */
|
|
800
|
-
/* @__PURE__ */
|
|
1114
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
1115
|
+
/* @__PURE__ */ jsx6(Text6, { children: "total " }),
|
|
1116
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.accent, children: sparkline(points.map((p) => p.total), 64) })
|
|
801
1117
|
] }),
|
|
802
|
-
/* @__PURE__ */
|
|
803
|
-
/* @__PURE__ */
|
|
804
|
-
/* @__PURE__ */
|
|
1118
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
1119
|
+
/* @__PURE__ */ jsx6(Text6, { children: "allowed " }),
|
|
1120
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.ok, children: sparkline(points.map((p) => p.allowed), 64) })
|
|
805
1121
|
] }),
|
|
806
|
-
/* @__PURE__ */
|
|
807
|
-
/* @__PURE__ */
|
|
808
|
-
/* @__PURE__ */
|
|
1122
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
1123
|
+
/* @__PURE__ */ jsx6(Text6, { children: "denied " }),
|
|
1124
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 64) })
|
|
809
1125
|
] })
|
|
810
1126
|
] }),
|
|
811
|
-
/* @__PURE__ */
|
|
812
|
-
/* @__PURE__ */
|
|
813
|
-
/* @__PURE__ */
|
|
1127
|
+
/* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
1128
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "Recent" }),
|
|
1129
|
+
/* @__PURE__ */ jsx6(
|
|
814
1130
|
Table,
|
|
815
1131
|
{
|
|
816
1132
|
columns: [
|
|
@@ -832,111 +1148,183 @@ function StatsPanel({ active: active2 }) {
|
|
|
832
1148
|
}
|
|
833
1149
|
|
|
834
1150
|
// src/tui/panels/Audit.tsx
|
|
835
|
-
import { Box as
|
|
836
|
-
import
|
|
837
|
-
import {
|
|
838
|
-
|
|
1151
|
+
import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
|
|
1152
|
+
import TextInput3 from "ink-text-input";
|
|
1153
|
+
import { useState as useState6 } from "react";
|
|
1154
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1155
|
+
var DECISIONS = [void 0, "DENY", "ALLOW"];
|
|
1156
|
+
var SIGNALS = [void 0, "dlp", "ratelimit"];
|
|
839
1157
|
function AuditPanel({ active: active2, focused }) {
|
|
840
|
-
const [
|
|
841
|
-
const
|
|
842
|
-
const
|
|
843
|
-
|
|
1158
|
+
const [di, setDi] = useState6(0);
|
|
1159
|
+
const [gi, setGi] = useState6(0);
|
|
1160
|
+
const [tool, setTool] = useState6("");
|
|
1161
|
+
const [agent, setAgent] = useState6("");
|
|
1162
|
+
const [search, setSearch] = useState6("");
|
|
1163
|
+
const [sel, setSel] = useState6(0);
|
|
1164
|
+
const [view, setView] = useState6("list");
|
|
1165
|
+
const [editing, setEditing] = useState6(null);
|
|
1166
|
+
const query = {
|
|
1167
|
+
filter: DECISIONS[di],
|
|
1168
|
+
signal: SIGNALS[gi],
|
|
1169
|
+
tool: tool || void 0,
|
|
1170
|
+
agent_name: agent || void 0,
|
|
1171
|
+
search: search || void 0,
|
|
1172
|
+
limit: 100
|
|
1173
|
+
};
|
|
1174
|
+
const auditQ = useLoader(() => api.audit.list(query), [di, gi, tool, agent, search]);
|
|
1175
|
+
usePoll(auditQ.reload, 6e3, active2 && view === "list" && !editing);
|
|
1176
|
+
const entries = auditQ.data?.entries ?? [];
|
|
1177
|
+
const current = entries[Math.min(sel, Math.max(0, entries.length - 1))];
|
|
1178
|
+
const sessionQ = useLoader(
|
|
1179
|
+
() => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
|
|
1180
|
+
[view, current?.session_id]
|
|
1181
|
+
);
|
|
844
1182
|
useInput4(
|
|
845
|
-
(input) => {
|
|
846
|
-
if (
|
|
1183
|
+
(input, key) => {
|
|
1184
|
+
if (view === "detail") {
|
|
1185
|
+
if (key.leftArrow || key.escape) setView("list");
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (key.upArrow) setSel((n) => Math.max(0, n - 1));
|
|
1189
|
+
else if (key.downArrow) setSel((n) => Math.min(entries.length - 1, n + 1));
|
|
1190
|
+
else if (key.return || key.rightArrow) {
|
|
1191
|
+
if (current) setView("detail");
|
|
1192
|
+
} else if (input === "f") setDi((n) => (n + 1) % DECISIONS.length);
|
|
1193
|
+
else if (input === "g") setGi((n) => (n + 1) % SIGNALS.length);
|
|
1194
|
+
else if (input === "t") setEditing("tool");
|
|
1195
|
+
else if (input === "n") setEditing("agent");
|
|
1196
|
+
else if (input === "/") setEditing("search");
|
|
1197
|
+
else if (input === "c") {
|
|
1198
|
+
setDi(0);
|
|
1199
|
+
setGi(0);
|
|
1200
|
+
setTool("");
|
|
1201
|
+
setAgent("");
|
|
1202
|
+
setSearch("");
|
|
1203
|
+
}
|
|
847
1204
|
},
|
|
848
|
-
{ isActive: focused }
|
|
1205
|
+
{ isActive: focused && !editing }
|
|
849
1206
|
);
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
/* @__PURE__ */
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1207
|
+
if (view === "detail" && current) {
|
|
1208
|
+
const sessionCalls = sessionQ.data?.entries ?? [];
|
|
1209
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1210
|
+
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1211
|
+
/* @__PURE__ */ jsx7(Text7, { color: decisionColor(current.decision), bold: true, children: current.decision }),
|
|
1212
|
+
/* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: " " + current.tool_name }),
|
|
1213
|
+
/* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " + current.permission + " \xB7 " + current.trust_level })
|
|
1214
|
+
] }),
|
|
1215
|
+
/* @__PURE__ */ jsx7(Detail, { label: "reason", value: current.reason ?? "\u2014" }),
|
|
1216
|
+
/* @__PURE__ */ jsx7(Detail, { label: "rule", value: current.matched_rule_id ?? "\u2014" }),
|
|
1217
|
+
/* @__PURE__ */ jsx7(Detail, { label: "agent", value: current.agent_name ?? "\u2014" }),
|
|
1218
|
+
/* @__PURE__ */ jsx7(Detail, { label: "session", value: current.session_id ?? "\u2014" }),
|
|
1219
|
+
/* @__PURE__ */ jsx7(Detail, { label: "dlp", value: current.dlp_matches?.length ? current.dlp_matches.join(", ") : "none", color: current.dlp_matches?.length ? theme.bad : void 0 }),
|
|
1220
|
+
/* @__PURE__ */ jsx7(Detail, { label: "when", value: new Date(current.created_at).toLocaleString() }),
|
|
1221
|
+
current.arguments_summary ? /* @__PURE__ */ jsx7(Detail, { label: "args", value: truncate(JSON.stringify(current.arguments_summary), 60) }) : null,
|
|
1222
|
+
/* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
1223
|
+
/* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
|
|
1224
|
+
"Session ",
|
|
1225
|
+
current.session_id ? `\xB7 ${sessionCalls.length} calls` : "(no session id)"
|
|
1226
|
+
] }),
|
|
1227
|
+
current.session_id ? /* @__PURE__ */ jsx7(
|
|
1228
|
+
Table,
|
|
1229
|
+
{
|
|
1230
|
+
columns: [
|
|
1231
|
+
{ header: "DECISION", width: 9 },
|
|
1232
|
+
{ header: "TOOL", width: 20 },
|
|
1233
|
+
{ header: "REASON", width: 26 },
|
|
1234
|
+
{ header: "WHEN", width: 6 }
|
|
1235
|
+
],
|
|
1236
|
+
rows: sessionCalls.slice(0, 12).map((e) => [
|
|
1237
|
+
{ value: e.decision, color: decisionColor(e.decision) },
|
|
1238
|
+
{ value: truncate(e.tool_name, 20), color: theme.accent },
|
|
1239
|
+
{ value: truncate(e.reason ?? "\u2014", 26) },
|
|
1240
|
+
{ value: ago(e.created_at), dim: true }
|
|
1241
|
+
])
|
|
1242
|
+
}
|
|
1243
|
+
) : null
|
|
1244
|
+
] }),
|
|
1245
|
+
/* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2190 back to list" })
|
|
1246
|
+
] });
|
|
1247
|
+
}
|
|
1248
|
+
const chip = (label, val, on) => /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
1249
|
+
/* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
|
|
1250
|
+
label,
|
|
1251
|
+
":"
|
|
1252
|
+
] }),
|
|
1253
|
+
/* @__PURE__ */ jsx7(Text7, { color: on ? theme.accentBright : theme.dim, children: val }),
|
|
1254
|
+
/* @__PURE__ */ jsx7(Text7, { children: " " })
|
|
1255
|
+
] });
|
|
1256
|
+
return /* @__PURE__ */ jsx7(DataView, { loading: auditQ.loading && !auditQ.data, error: auditQ.error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1257
|
+
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1258
|
+
chip("dec", DECISIONS[di] ?? "all", di !== 0),
|
|
1259
|
+
chip("sig", SIGNALS[gi] ?? "all", gi !== 0),
|
|
1260
|
+
chip("tool", tool || "\xB7", !!tool),
|
|
1261
|
+
chip("agent", agent || "\xB7", !!agent),
|
|
1262
|
+
chip("search", search || "\xB7", !!search)
|
|
857
1263
|
] }),
|
|
858
|
-
/* @__PURE__ */
|
|
1264
|
+
/* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: focused ? "\u2191\u2193 \xB7 enter detail \xB7 f dec \xB7 g sig \xB7 t tool \xB7 n agent \xB7 / search \xB7 c clear" : "press \u2192 to browse" }),
|
|
1265
|
+
editing ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
|
|
1266
|
+
/* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
|
|
1267
|
+
editing,
|
|
1268
|
+
": "
|
|
1269
|
+
] }),
|
|
1270
|
+
/* @__PURE__ */ jsx7(
|
|
1271
|
+
TextInput3,
|
|
1272
|
+
{
|
|
1273
|
+
value: editing === "tool" ? tool : editing === "agent" ? agent : search,
|
|
1274
|
+
onChange: editing === "tool" ? setTool : editing === "agent" ? setAgent : setSearch,
|
|
1275
|
+
onSubmit: () => setEditing(null)
|
|
1276
|
+
}
|
|
1277
|
+
)
|
|
1278
|
+
] }) : null,
|
|
1279
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
|
|
1280
|
+
auditQ.data?.total ?? 0,
|
|
1281
|
+
" matched \xB7 showing ",
|
|
1282
|
+
Math.min(entries.length, 12)
|
|
1283
|
+
] }) }),
|
|
1284
|
+
/* @__PURE__ */ jsx7(
|
|
859
1285
|
Table,
|
|
860
1286
|
{
|
|
861
1287
|
columns: [
|
|
1288
|
+
{ header: "", width: 2 },
|
|
862
1289
|
{ header: "DECISION", width: 9 },
|
|
863
1290
|
{ header: "TOOL", width: 18 },
|
|
864
1291
|
{ header: "AGENT", width: 14 },
|
|
865
|
-
{ header: "REASON", width:
|
|
1292
|
+
{ header: "REASON", width: 24 },
|
|
866
1293
|
{ header: "DLP", width: 4 },
|
|
867
1294
|
{ header: "WHEN", width: 6 }
|
|
868
1295
|
],
|
|
869
|
-
rows: entries.map((e) =>
|
|
870
|
-
{ value: e.decision, color: decisionColor(e.decision) },
|
|
871
|
-
{ value: truncate(e.tool_name, 18), color: theme.accent },
|
|
872
|
-
{ value: truncate(e.agent_name ?? "\u2014", 14), dim: true },
|
|
873
|
-
{ value: truncate(e.reason ?? "\u2014", 26) },
|
|
874
|
-
{ value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
|
|
875
|
-
{ value: ago(e.created_at), dim: true }
|
|
876
|
-
])
|
|
1296
|
+
rows: entries.slice(0, 12).map((e, i) => rowFor(e, i === sel && focused))
|
|
877
1297
|
}
|
|
878
|
-
)
|
|
1298
|
+
)
|
|
879
1299
|
] }) });
|
|
880
1300
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
/* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
|
|
898
|
-
data.counts.idle,
|
|
899
|
-
" idle"
|
|
900
|
-
] }),
|
|
901
|
-
" \xB7 ",
|
|
902
|
-
data.counts.deactivated,
|
|
903
|
-
" off"
|
|
904
|
-
] }) : null,
|
|
905
|
-
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
|
|
906
|
-
Table,
|
|
907
|
-
{
|
|
908
|
-
columns: [
|
|
909
|
-
{ header: "STATUS", width: 7 },
|
|
910
|
-
{ header: "AGENT", width: 20 },
|
|
911
|
-
{ header: "CALLS", width: 6 },
|
|
912
|
-
{ header: "DENY", width: 5 },
|
|
913
|
-
{ header: "DLP", width: 4 },
|
|
914
|
-
{ header: "TRUST", width: 6 },
|
|
915
|
-
{ header: "CHARACTER", width: 20 }
|
|
916
|
-
],
|
|
917
|
-
rows: rows.map((a) => [
|
|
918
|
-
{ value: a.status, color: modeColor(a.status) },
|
|
919
|
-
{ value: truncate(a.agent_name ?? a.agent_id ?? a.session_id, 20), color: theme.accent },
|
|
920
|
-
{ value: String(a.total_calls) },
|
|
921
|
-
{ value: String(a.denied_calls), color: a.denied_calls ? theme.bad : void 0, dim: !a.denied_calls },
|
|
922
|
-
{ value: String(a.dlp_events), color: a.dlp_events ? theme.bad : void 0, dim: !a.dlp_events },
|
|
923
|
-
{ value: `${a.trust_score}` },
|
|
924
|
-
{ value: truncate(a.character || "\u2014", 20), dim: true }
|
|
925
|
-
])
|
|
926
|
-
}
|
|
927
|
-
) })
|
|
928
|
-
] }) });
|
|
1301
|
+
function rowFor(e, active2) {
|
|
1302
|
+
return [
|
|
1303
|
+
{ value: active2 ? "\u25B8" : "", color: theme.accentBright },
|
|
1304
|
+
{ value: e.decision, color: decisionColor(e.decision) },
|
|
1305
|
+
{ value: truncate(e.tool_name, 18), color: theme.accent },
|
|
1306
|
+
{ value: truncate(e.agent_name ?? "\u2014", 14), dim: true },
|
|
1307
|
+
{ value: truncate(e.reason ?? "\u2014", 24) },
|
|
1308
|
+
{ value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
|
|
1309
|
+
{ value: ago(e.created_at), dim: true }
|
|
1310
|
+
];
|
|
1311
|
+
}
|
|
1312
|
+
function Detail({ label, value, color }) {
|
|
1313
|
+
return /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1314
|
+
/* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: label.padEnd(9) }),
|
|
1315
|
+
/* @__PURE__ */ jsx7(Text7, { color, children: value })
|
|
1316
|
+
] });
|
|
929
1317
|
}
|
|
930
1318
|
|
|
931
1319
|
// src/tui/App.tsx
|
|
932
1320
|
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
933
1321
|
var SECTIONS = [
|
|
1322
|
+
{ label: "Live", Panel: LivePanel },
|
|
934
1323
|
{ label: "Policies", Panel: PoliciesPanel },
|
|
935
1324
|
{ label: "Rate Limit", Panel: RateLimitPanel },
|
|
936
1325
|
{ label: "DLP", Panel: DlpPanel },
|
|
937
1326
|
{ label: "Stats", Panel: StatsPanel },
|
|
938
|
-
{ label: "Audit", Panel: AuditPanel }
|
|
939
|
-
{ label: "Agents", Panel: AgentsPanel }
|
|
1327
|
+
{ label: "Audit", Panel: AuditPanel }
|
|
940
1328
|
];
|
|
941
1329
|
var BANNER_HEX = ["#1432A0", "#2850BE", "#3C6ED7", "#5A8CE6", "#82AAF0", "#AAC8FA"];
|
|
942
1330
|
function Banner() {
|
|
@@ -954,8 +1342,8 @@ function Banner() {
|
|
|
954
1342
|
}
|
|
955
1343
|
function App() {
|
|
956
1344
|
const { exit } = useApp();
|
|
957
|
-
const [section, setSection] =
|
|
958
|
-
const [focus, setFocus] =
|
|
1345
|
+
const [section, setSection] = useState7(0);
|
|
1346
|
+
const [focus, setFocus] = useState7("nav");
|
|
959
1347
|
useInput5((input, key) => {
|
|
960
1348
|
if (focus === "nav") {
|
|
961
1349
|
if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
|