prae-cli 0.7.0 → 0.8.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/lib/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import "./evaluate-CkYySl7T.js";
3
- import "./views-UODslKwu.js";
4
- import { t as main } from "./cli-chtNj_el.js";
2
+ import "./evaluate-wMPkjcIC.js";
3
+ import "./views-CCwVXpo2.js";
4
+ import { t as main } from "./cli-UAE968If.js";
5
5
  import "./src-Ciggx9ja.js";
6
6
 
7
7
  //#region src/bin.ts
@@ -0,0 +1,99 @@
1
+ import { d as canonical, i as fingerprint } from "./src-Ciggx9ja.js";
2
+ import { createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
3
+
4
+ //#region src/checkpoint.ts
5
+ /** What the signature covers: the facts, never the timestamp or the name of the key. */
6
+ function signable(cp) {
7
+ return Buffer.from(canonical({
8
+ ledger: cp.ledger,
9
+ entries: cp.entries,
10
+ head: cp.head
11
+ }));
12
+ }
13
+ function makeCheckpoint(rows, ledger, privateKeyPem, at = (/* @__PURE__ */ new Date()).toISOString()) {
14
+ const head = rows.at(-1);
15
+ const cp = {
16
+ prae: "checkpoint/1",
17
+ at,
18
+ ledger,
19
+ entries: rows.length,
20
+ head: head === void 0 ? {
21
+ seq: -1,
22
+ hash: ""
23
+ } : {
24
+ seq: head.seq,
25
+ hash: head.hash
26
+ }
27
+ };
28
+ if (privateKeyPem === void 0) return cp;
29
+ const key = createPrivateKey(privateKeyPem);
30
+ const publicKey = createPublicKey(key).export({
31
+ type: "spki",
32
+ format: "pem"
33
+ }).toString();
34
+ return {
35
+ ...cp,
36
+ fingerprint: fingerprint(publicKey),
37
+ sig: sign(null, signable(cp), key).toString("base64")
38
+ };
39
+ }
40
+ /**
41
+ * The ledger as it is now, against the checkpoint as it was then. Rows may
42
+ * have been appended since; nothing before the head may differ.
43
+ */
44
+ function checkCheckpoint(rows, cp, publicKeyPem) {
45
+ const lines = [];
46
+ let ok = true;
47
+ if (cp.prae !== "checkpoint/1") return {
48
+ ok: false,
49
+ lines: [`not a checkpoint: prae is ${String(cp.prae)}`]
50
+ };
51
+ if (publicKeyPem !== void 0) if (cp.sig === void 0) {
52
+ ok = false;
53
+ lines.push("checkpoint is not signed; a key was given to check it against");
54
+ } else {
55
+ let good = false;
56
+ try {
57
+ good = verify(null, signable(cp), createPublicKey(publicKeyPem), Buffer.from(cp.sig, "base64"));
58
+ } catch {
59
+ good = false;
60
+ }
61
+ if (good) lines.push(`checkpoint signature verifies against ${fingerprint(publicKeyPem)}`);
62
+ else {
63
+ ok = false;
64
+ lines.push(`checkpoint signature FAILS against ${fingerprint(publicKeyPem)} — the checkpoint itself has been altered or was signed by another key`);
65
+ }
66
+ }
67
+ if (rows.length < cp.entries) {
68
+ ok = false;
69
+ lines.push(`ledger holds ${rows.length} rows; the checkpoint of ${cp.at} recorded ${cp.entries} — ${cp.entries - rows.length} row${cp.entries - rows.length === 1 ? "" : "s"} removed since`);
70
+ return {
71
+ ok,
72
+ lines
73
+ };
74
+ }
75
+ if (cp.entries === 0) {
76
+ lines.push(`checkpoint of ${cp.at} recorded an empty ledger; ${rows.length} rows since`);
77
+ return {
78
+ ok,
79
+ lines
80
+ };
81
+ }
82
+ const then = rows.find((row) => row.seq === cp.head.seq);
83
+ if (then === void 0 || then.hash !== cp.head.hash) {
84
+ ok = false;
85
+ lines.push(`row ${cp.head.seq} does not match the checkpoint head (${cp.head.hash.slice(0, 16)}…) — the ledger was rewritten since ${cp.at}`);
86
+ return {
87
+ ok,
88
+ lines
89
+ };
90
+ }
91
+ lines.push(`checkpoint of ${cp.at} holds: ${cp.entries} rows then, ${rows.length} now, head ${cp.head.hash.slice(0, 16)}… intact`);
92
+ return {
93
+ ok,
94
+ lines
95
+ };
96
+ }
97
+
98
+ //#endregion
99
+ export { checkCheckpoint, makeCheckpoint };
@@ -1,5 +1,5 @@
1
- import { t as evaluate } from "./evaluate-CkYySl7T.js";
2
- import { a as renderPacks, c as renderNav, d as FRAMEWORKS, f as cite, i as renderOverview, l as renderShell, n as renderApprovals, o as CONTROLS, r as renderCompliance, s as esc, t as navFor, u as VERDICT_LABEL$1 } from "./views-UODslKwu.js";
1
+ import { t as evaluate } from "./evaluate-wMPkjcIC.js";
2
+ import { a as renderPacks, c as renderNav, d as FRAMEWORKS, f as cite, i as renderOverview, l as renderShell, n as renderApprovals, o as CONTROLS, r as renderCompliance, s as esc, t as navFor, u as VERDICT_LABEL$1 } from "./views-CCwVXpo2.js";
3
3
  import { f as chain, i as fingerprint, l as verifySignature, m as seal, n as readEntries, r as verifyEntries, t as Ledger, u as GENESIS } from "./src-Ciggx9ja.js";
4
4
  import { existsSync, readFileSync, readdirSync, statSync, watch } from "node:fs";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -72,9 +72,11 @@ function readCommandMatch(check$1, path, value) {
72
72
  check$1.keys(path, value, [
73
73
  "name",
74
74
  "token",
75
- "pipedTo"
75
+ "anyToken",
76
+ "pipedTo",
77
+ "pipeline"
76
78
  ]);
77
- if (value.name === void 0 && value.token === void 0 && value.pipedTo === void 0) check$1.fail(path, "expected `name`, `token` or `pipedTo`; a command matcher with none matches nothing");
79
+ if (value.name === void 0 && value.token === void 0 && value.anyToken === void 0 && value.pipedTo === void 0 && value.pipeline === void 0) check$1.fail(path, "expected `name`, `token`, `anyToken`, `pipedTo` or `pipeline`; a command matcher with none matches nothing");
78
80
  const out = {};
79
81
  if (value.token !== void 0) {
80
82
  const tokens = check$1.names(`${path}.token`, value.token);
@@ -84,10 +86,18 @@ function readCommandMatch(check$1, path, value) {
84
86
  const names = check$1.names(`${path}.name`, value.name);
85
87
  if (names !== void 0) out.name = names;
86
88
  }
89
+ if (value.anyToken !== void 0) {
90
+ const tokens = check$1.names(`${path}.anyToken`, value.anyToken);
91
+ if (tokens !== void 0) out.anyToken = tokens;
92
+ }
87
93
  if (value.pipedTo !== void 0) {
88
94
  const pipes = check$1.names(`${path}.pipedTo`, value.pipedTo);
89
95
  if (pipes !== void 0) out.pipedTo = pipes;
90
96
  }
97
+ if (value.pipeline !== void 0) {
98
+ const shapes = check$1.names(`${path}.pipeline`, value.pipeline);
99
+ if (shapes !== void 0) out.pipeline = shapes;
100
+ }
91
101
  return out;
92
102
  }
93
103
  function readPathMatch(check$1, path, value) {
@@ -962,7 +972,6 @@ function verifyRecord(record, tamper, repair = false) {
962
972
  signatures
963
973
  };
964
974
  }
965
- const OWASP = "owasp-llm-top-10-2026";
966
975
  /**
967
976
  * Evidence toward each control, from the pack and the ledger as they are
968
977
  * now — the rows the controls package defines, so the console, the exporter and
@@ -991,15 +1000,16 @@ function complianceOf(record) {
991
1000
  verification: verifyEntries(text),
992
1001
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
993
1002
  };
994
- const owasp = FRAMEWORKS[OWASP]?.controls ?? {};
995
1003
  return { controls: CONTROLS.map((control) => {
996
- const code = /LLM\d\d/.exec(control.id)?.[0];
997
- const named = code === void 0 ? void 0 : owasp[code];
1004
+ const found = Object.entries(FRAMEWORKS).find(([, f]) => control.id.startsWith(`${f.short} · `));
1005
+ const code = found === void 0 ? void 0 : control.id.split(" · ")[1];
1006
+ const named = found === void 0 || code === void 0 ? void 0 : found[1].controls[code];
998
1007
  return {
999
1008
  id: code ?? control.id,
1000
1009
  ...named === void 0 ? {} : { name: named.name },
1001
1010
  asks: control.asks,
1002
- framework: named === void 0 ? "obligation" : OWASP,
1011
+ framework: found === void 0 ? "obligation" : found[0],
1012
+ ...found === void 0 ? {} : { frameworkName: found[1].name },
1003
1013
  ...control.evidence(data)
1004
1014
  };
1005
1015
  }) };
@@ -1177,9 +1187,23 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
1177
1187
  the exit code is non-zero if any invariant no longer holds, so a
1178
1188
  control pack can be defended in CI. --pack overrides the suite's own.
1179
1189
 
1180
- prae verify <ledger.jsonl> [--key <public.pem>]
1190
+ prae verify <ledger.jsonl> [--key <public.pem>] [--checkpoint <file>]
1181
1191
  Re-derive every hash in a decision log and report any break. With
1182
- --key, also verify each row's signature against that pinned key.
1192
+ --key, also verify each row's signature against that pinned key. With
1193
+ --checkpoint, also check that nothing before that checkpoint's head
1194
+ has been removed or rewritten since it was taken.
1195
+
1196
+ prae checkpoint <ledger.jsonl> [--key <private.pem>] [--out <file>]
1197
+ Record the ledger's length and head, signed, to keep somewhere the
1198
+ ledger's machine cannot write. A chain proves no row inside the file
1199
+ was touched; a checkpoint proves the file was not cut at the end.
1200
+
1201
+ prae export <ledger.jsonl> --format ocsf [--out <file>]
1202
+ [--hec <url> --hec-token <token>]
1203
+ The record as OCSF 1.5.0 events (NDJSON) for a SIEM or data lake:
1204
+ refusals, holds and withheld outputs as Detection Findings, allowed
1205
+ calls as API Activity, every event carrying the row's seal. With --hec,
1206
+ post them to a Splunk HTTP Event Collector instead of writing a file.
1183
1207
 
1184
1208
  prae keygen [--out <dir>]
1185
1209
  Generate an Ed25519 keypair for signing ledger rows. Prints the
@@ -1260,7 +1284,7 @@ async function check(path) {
1260
1284
  * from the findings, so there is nothing here to be careful about printing.
1261
1285
  */
1262
1286
  async function scanFile(path, packPath, json) {
1263
- const { scan } = await import("./src-Vnr0fcpK.js");
1287
+ const { scan } = await import("./src-CwBEFhww.js");
1264
1288
  let content;
1265
1289
  try {
1266
1290
  content = await readFile(path, "utf8");
@@ -1300,7 +1324,7 @@ async function scanFile(path, packPath, json) {
1300
1324
  * not come up inspecting nothing.
1301
1325
  */
1302
1326
  async function mcp(root, packPath, ledgerPath) {
1303
- const { serveMcp } = await import("./mcp-Bq0DNPPR.js");
1327
+ const { serveMcp } = await import("./mcp-doNYU-1C.js");
1304
1328
  const { resolve: resolve$1 } = await import("node:path");
1305
1329
  const bundled = [join(import.meta.dirname, "secret-catcher.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "secret-catcher.prae.json")].find((candidate) => existsSync(candidate));
1306
1330
  const chosen = packPath ?? bundled;
@@ -1404,7 +1428,7 @@ async function receipt(path, seq, keyPath, out) {
1404
1428
  * file and sets the exit code, so scripts can branch on it.
1405
1429
  */
1406
1430
  async function verifyReceiptCmd(path) {
1407
- const { checkReceipt } = await import("./receipt-check-DrDE3Wwi.js");
1431
+ const { checkReceipt } = await import("./receipt-check-s3Aryyz-.js");
1408
1432
  let parsed;
1409
1433
  try {
1410
1434
  parsed = JSON.parse(await readFile(path, "utf8"));
@@ -1435,7 +1459,7 @@ async function test(suitePath, packOverride, asJson) {
1435
1459
  } catch (error) {
1436
1460
  return fail(`${suitePath} is not valid JSON: ${error instanceof Error ? error.message : error}`);
1437
1461
  }
1438
- const { parseSuite, renderSuite, runSuite } = await import("./test-run-04ubUcSb.js");
1462
+ const { parseSuite, renderSuite, runSuite } = await import("./test-run-XNIRHKOu.js");
1439
1463
  const suite = parseSuite(parsed);
1440
1464
  if (!suite.ok) {
1441
1465
  console.error(`prae: ${suitePath} is not a valid test suite`);
@@ -1446,7 +1470,7 @@ async function test(suitePath, packOverride, asJson) {
1446
1470
  if (chosen === void 0) return fail("test needs a pack: set `pack` in the suite or pass --pack");
1447
1471
  const pack = await loadPack(chosen);
1448
1472
  const { capabilityForTool } = await import("capmark");
1449
- const { scan } = await import("./src-Vnr0fcpK.js");
1473
+ const { scan } = await import("./src-CwBEFhww.js");
1450
1474
  const inspect = pack.rules.some((rule) => rule.when.secrets !== void 0) ? (path) => {
1451
1475
  try {
1452
1476
  return scan(readFileSync(path, "utf8"));
@@ -1462,7 +1486,7 @@ async function test(suitePath, packOverride, asJson) {
1462
1486
  else for (const line of renderSuite(result)) console.log(line);
1463
1487
  if (!result.ok) process.exit(1);
1464
1488
  }
1465
- async function verify(path, keyPath) {
1489
+ async function verify(path, keyPath, checkpointPath) {
1466
1490
  let ledger;
1467
1491
  try {
1468
1492
  ledger = Ledger.open(path);
@@ -1474,6 +1498,13 @@ async function verify(path, keyPath) {
1474
1498
  const entries = ledger.read();
1475
1499
  const last = entries.at(-1);
1476
1500
  console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
1501
+ if (checkpointPath !== void 0) {
1502
+ const { checkCheckpoint } = await import("./checkpoint-DiznOVpS.js");
1503
+ const publicKey = keyPath === void 0 ? void 0 : readFileSync(keyPath, "utf8");
1504
+ const verdict = checkCheckpoint(entries, JSON.parse(readFileSync(checkpointPath, "utf8")), publicKey);
1505
+ for (const line of verdict.lines) (verdict.ok ? console.log : console.error)(`checkpoint: ${line}`);
1506
+ if (!verdict.ok) process.exitCode = 1;
1507
+ }
1477
1508
  if (keyPath !== void 0) {
1478
1509
  const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
1479
1510
  const { readFileSync: readFileSync$1 } = await import("node:fs");
@@ -1553,7 +1584,7 @@ async function report(path, out, packPath) {
1553
1584
  * that step is a proposal a human approves, and it starts from this list.
1554
1585
  */
1555
1586
  async function instructions(from, asJson) {
1556
- const { ingest } = await import("./src-CraUX64d.js");
1587
+ const { ingest } = await import("./src-d4c_n6wd.js");
1557
1588
  const result = ingest(from);
1558
1589
  if (asJson) {
1559
1590
  console.log(JSON.stringify(result, null, 2));
@@ -1589,7 +1620,7 @@ async function instructions(from, asJson) {
1589
1620
  * on purpose rather than something that happened to their policy.
1590
1621
  */
1591
1622
  async function propose(from, options) {
1592
- const [{ ingest }, { extract }] = await Promise.all([import("./src-CraUX64d.js"), import("./src-CQ0UmFUK.js")]);
1623
+ const [{ ingest }, { extract }] = await Promise.all([import("./src-d4c_n6wd.js"), import("./src-CBGZVvt7.js")]);
1593
1624
  const read = ingest(from);
1594
1625
  if (read.files.length === 0) {
1595
1626
  console.log(`No instruction files found at or above ${from}.`);
@@ -1649,9 +1680,9 @@ async function consoleOut(path, argv) {
1649
1680
  const instructionsDir = flag(argv, "instructions");
1650
1681
  if (instructionsDir !== void 0) {
1651
1682
  const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
1652
- import("./src-CraUX64d.js"),
1653
- import("./src-CQ0UmFUK.js"),
1654
- import("./instructions-C104EUaE.js")
1683
+ import("./src-d4c_n6wd.js"),
1684
+ import("./src-CBGZVvt7.js"),
1685
+ import("./instructions-DulxSqFd.js")
1655
1686
  ]);
1656
1687
  const read = ingest(instructionsDir);
1657
1688
  instructionsView = renderInstructions(data, read, extract(read.statements));
@@ -1749,6 +1780,60 @@ async function init(argv) {
1749
1780
  return fail(error instanceof Error ? error.message : String(error));
1750
1781
  }
1751
1782
  }
1783
+ async function checkpoint(path, keyPath, out) {
1784
+ const { makeCheckpoint } = await import("./checkpoint-DiznOVpS.js");
1785
+ let ledger;
1786
+ try {
1787
+ ledger = Ledger.open(path);
1788
+ } catch (error) {
1789
+ return fail(error instanceof Error ? error.message : String(error));
1790
+ }
1791
+ const chain$1 = ledger.verify();
1792
+ if (!chain$1.ok) return fail(`chain broken at line ${chain$1.problems[0]?.line}: ${chain$1.problems[0]?.message} — a checkpoint of a broken chain would certify the break`);
1793
+ const privateKey = keyPath === void 0 ? void 0 : readFileSync(keyPath, "utf8");
1794
+ const cp = makeCheckpoint(ledger.read(), path.split("/").at(-1) ?? path, privateKey);
1795
+ const text = `${JSON.stringify(cp, null, 2)}\n`;
1796
+ if (out === void 0) console.log(text);
1797
+ else {
1798
+ await mkdir(dirname(out), { recursive: true });
1799
+ await writeFile(out, text);
1800
+ console.log(`checkpoint ${out} · ${cp.entries} rows · head ${cp.head.hash.slice(0, 16)}…${cp.sig === void 0 ? " · unsigned" : ` · signed ${cp.fingerprint}`}`);
1801
+ console.log("Keep it where this machine cannot write. `prae verify --checkpoint` later detects rows removed or rewritten since.");
1802
+ }
1803
+ }
1804
+ async function exportLedger(path, options) {
1805
+ const { toOcsf, ndjson, sendToHec } = await import("./ocsf-DhVGbBUw.js");
1806
+ const { createRequire } = await import("node:module");
1807
+ const { version } = createRequire(import.meta.url)("../package.json");
1808
+ let ledger;
1809
+ try {
1810
+ ledger = Ledger.open(path);
1811
+ } catch (error) {
1812
+ return fail(error instanceof Error ? error.message : String(error));
1813
+ }
1814
+ const events = toOcsf(ledger.read(), {
1815
+ product: {
1816
+ name: "prae-gate",
1817
+ version
1818
+ },
1819
+ ledger: path
1820
+ });
1821
+ if (options.hec !== void 0) {
1822
+ if (options.token === void 0) fail("--hec needs --hec-token");
1823
+ const sent = await sendToHec(events, options.hec, options.token);
1824
+ if (sent.status < 200 || sent.status >= 300) return fail(`HEC answered ${sent.status}: ${sent.body.slice(0, 200)}`);
1825
+ console.log(`${sent.sent} events → ${options.hec} (${sent.status})`);
1826
+ return;
1827
+ }
1828
+ const text = ndjson(events);
1829
+ if (options.out === void 0) {
1830
+ process.stdout.write(text);
1831
+ return;
1832
+ }
1833
+ await mkdir(dirname(options.out), { recursive: true });
1834
+ await writeFile(options.out, text);
1835
+ console.log(`${events.length} OCSF events → ${options.out} · seals carried under unmapped; the signature covers the ledger row, not the event`);
1836
+ }
1752
1837
  async function main(argv) {
1753
1838
  const [command, ...rest] = argv;
1754
1839
  switch (command) {
@@ -1788,7 +1873,23 @@ async function main(argv) {
1788
1873
  case "verify": {
1789
1874
  const path = rest[0];
1790
1875
  if (path === void 0) fail("verify needs a ledger file");
1791
- return verify(path, flag(rest, "key"));
1876
+ return verify(path, flag(rest, "key"), flag(rest, "checkpoint"));
1877
+ }
1878
+ case "checkpoint": {
1879
+ const path = rest[0];
1880
+ if (path === void 0) fail("checkpoint needs a ledger file");
1881
+ return checkpoint(path, flag(rest, "key"), flag(rest, "out"));
1882
+ }
1883
+ case "export": {
1884
+ const path = rest[0];
1885
+ if (path === void 0) fail("export needs a ledger file");
1886
+ const format = flag(rest, "format") ?? "ocsf";
1887
+ if (format !== "ocsf") fail(`unknown format ${format}; only ocsf today`);
1888
+ return exportLedger(path, {
1889
+ out: flag(rest, "out"),
1890
+ hec: flag(rest, "hec"),
1891
+ token: flag(rest, "hec-token")
1892
+ });
1792
1893
  }
1793
1894
  case "verify-receipt": {
1794
1895
  const path = rest[0];
@@ -123,6 +123,22 @@ function matches$1(pattern, value) {
123
123
  if (pattern.endsWith("*")) return value.startsWith(pattern.slice(0, -1));
124
124
  return value === pattern;
125
125
  }
126
+ /**
127
+ * A glob with `*` anywhere, for pipeline signatures: `curl*|*sh` is the
128
+ * shape of download-and-execute however the two ends are spelled.
129
+ */
130
+ function globMatches(pattern, value) {
131
+ const source = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
132
+ return (/* @__PURE__ */ new RegExp(`^${source}$`)).test(value);
133
+ }
134
+ /**
135
+ * Every wanted token holds in a segment: a plain pattern must match some
136
+ * token, a `!pattern` must match none. `["-rf", "!--dry-run"]` is a real
137
+ * force-delete, not a rehearsal of one.
138
+ */
139
+ function tokensHold(wanted, tokens) {
140
+ return wanted.every((pattern) => pattern.startsWith("!") ? !tokens.some((token) => matches$1(pattern.slice(1), token)) : tokens.some((token) => matches$1(pattern, token)));
141
+ }
126
142
  const list = (v) => v === void 0 ? [] : typeof v === "string" ? [v] : v;
127
143
  /**
128
144
  * Judge one command line against one matcher. Every present field must hold.
@@ -132,17 +148,24 @@ function commandMatches(match, command) {
132
148
  if (parts.length === 0) return false;
133
149
  const names = list(match.name);
134
150
  const args = list(match.token);
151
+ const anyArgs = list(match.anyToken);
135
152
  const pipes = list(match.pipedTo);
153
+ const shapes = list(match.pipeline);
154
+ const argsHold = (segment) => tokensHold(args, segment.tokens) && (anyArgs.length === 0 || anyArgs.some((wanted) => segment.tokens.some((token) => matches$1(wanted, token))));
136
155
  if (names.length > 0) {
137
156
  const hit = parts.filter((segment) => names.some((pattern) => matches$1(pattern, segment.name) || segment.wrappers.some((wrapper) => matches$1(pattern, wrapper))));
138
157
  if (hit.length === 0) return false;
139
- if (args.length > 0 && !hit.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
140
- } else if (args.length > 0) {
141
- if (!parts.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
158
+ if ((args.length > 0 || anyArgs.length > 0) && !hit.some(argsHold)) return false;
159
+ } else if (args.length > 0 || anyArgs.length > 0) {
160
+ if (!parts.some(argsHold)) return false;
142
161
  }
143
162
  if (pipes.length > 0) {
144
163
  if (!parts.slice(1).some((segment) => pipes.some((pattern) => matches$1(pattern, segment.name)))) return false;
145
164
  }
165
+ if (shapes.length > 0) {
166
+ const signature = parts.map((segment) => segment.name).join("|");
167
+ if (!shapes.some((pattern) => globMatches(pattern, signature))) return false;
168
+ }
146
169
  return true;
147
170
  }
148
171
 
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import "./evaluate-CkYySl7T.js";
2
- import "./views-UODslKwu.js";
3
- import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-chtNj_el.js";
1
+ import "./evaluate-wMPkjcIC.js";
2
+ import "./views-CCwVXpo2.js";
3
+ import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-UAE968If.js";
4
4
  import "./src-Ciggx9ja.js";
5
5
 
6
6
  export { applyWrite, defaultProfile, dshHome, main, planWrite, praeRow, profileAt };
@@ -1,4 +1,4 @@
1
- import { c as renderNav, l as renderShell, s as esc, t as navFor } from "./views-UODslKwu.js";
1
+ import { c as renderNav, l as renderShell, s as esc, t as navFor } from "./views-CCwVXpo2.js";
2
2
 
3
3
  //#region src/console/instructions.ts
4
4
  const STYLE = `
@@ -1,5 +1,5 @@
1
- import { t as evaluate } from "./evaluate-CkYySl7T.js";
2
- import { a as scan, n as MAX_SCAN_BYTES } from "./src-Br1tGPtS.js";
1
+ import { t as evaluate } from "./evaluate-wMPkjcIC.js";
2
+ import { a as scan, n as MAX_SCAN_BYTES } from "./src-B25SHWcz.js";
3
3
  import { readFile, readdir, stat } from "node:fs/promises";
4
4
  import { resolve, sep } from "node:path";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -0,0 +1,211 @@
1
+ import { basename } from "node:path";
2
+
3
+ //#region src/ocsf.ts
4
+ const OCSF_VERSION = "1.5.0";
5
+ const DETECTION_FINDING = 2004;
6
+ const API_ACTIVITY = 6003;
7
+ /** OCSF `severity_id`. */
8
+ const SEVERITY = {
9
+ informational: 1,
10
+ low: 2,
11
+ medium: 3,
12
+ high: 4
13
+ };
14
+ /** OCSF `action_id`: what was done about it, in the schema's four words. */
15
+ const ACTION = {
16
+ allowed: 1,
17
+ denied: 2,
18
+ observed: 3
19
+ };
20
+ /** OCSF API Activity `activity_id`. */
21
+ const API_ACTIVITY_OF = {
22
+ read: 2,
23
+ list_dir: 2,
24
+ read_file: 2,
25
+ web_fetch: 2,
26
+ write: 3,
27
+ edit: 3,
28
+ str_replace_editor: 3
29
+ };
30
+ function isEvent(row) {
31
+ return row.tool.startsWith("prae:");
32
+ }
33
+ const isUrl = (subject) => /^https?:\/\//.test(subject);
34
+ /** The file or URL the call was aimed at, as an OCSF evidence artifact. */
35
+ function evidences(row) {
36
+ const subject = row.subject;
37
+ if (subject === void 0 || subject === "" || isEvent(row)) return [];
38
+ if (isUrl(subject)) return [{ url: {
39
+ url_string: subject,
40
+ hostname: subject.replace(/^https?:\/\//, "").split("/")[0] ?? ""
41
+ } }];
42
+ if (row.command !== void 0) return [];
43
+ return [{ file: {
44
+ name: basename(subject),
45
+ path: subject,
46
+ type_id: 1
47
+ } }];
48
+ }
49
+ /** The same target, as the resource an API Activity touched. */
50
+ function resources(row) {
51
+ const subject = row.subject;
52
+ if (subject === void 0 || subject === "") return [];
53
+ if (row.command !== void 0) return [{
54
+ name: row.command.program,
55
+ type: "program"
56
+ }];
57
+ return [{
58
+ name: subject,
59
+ type: isUrl(subject) ? "url" : "file"
60
+ }];
61
+ }
62
+ /** Everything the SIEM has no field for, kept verbatim. The seal lives here. */
63
+ function unmapped(row, disposition) {
64
+ return {
65
+ seq: row.seq,
66
+ disposition,
67
+ seal: row.hash,
68
+ prev: row.prev,
69
+ ...row.sig === void 0 ? {} : { sig: row.sig },
70
+ verdict: row.verdict,
71
+ ...row.rule === void 0 ? {} : { rule: row.rule },
72
+ ...row.capability === void 0 ? {} : { capability: row.capability },
73
+ ...row.phase === void 0 ? {} : { phase: row.phase },
74
+ ...row.shadow === void 0 ? {} : { shadow: row.shadow },
75
+ ...row.subject === void 0 ? {} : { subject: row.subject },
76
+ ...row.command === void 0 ? {} : { command: row.command },
77
+ ...row.secrets === void 0 ? {} : { secrets: row.secrets.map((s) => ({
78
+ type: s.type,
79
+ rule: s.rule
80
+ })) },
81
+ ...row.micros === void 0 ? {} : { micros: row.micros },
82
+ pack: row.pack,
83
+ trace: row.trace,
84
+ evidence: "PRAE ledger row. The seal and signature cover the original row, not this event; verify with `prae verify` or a receipt."
85
+ };
86
+ }
87
+ function metadata(row, options) {
88
+ const log = options.ledger === void 0 ? void 0 : basename(options.ledger);
89
+ return {
90
+ product: {
91
+ name: options.product.name,
92
+ vendor_name: "PRAE",
93
+ version: options.product.version
94
+ },
95
+ version: OCSF_VERSION,
96
+ profiles: ["security_control"],
97
+ uid: `${row.session ?? log ?? "ledger"}#${row.seq}`,
98
+ ...row.session === void 0 ? {} : { correlation_uid: row.session },
99
+ ...log === void 0 ? {} : { log_name: log },
100
+ logged_time: Date.parse(row.at)
101
+ };
102
+ }
103
+ /** What happened, in one line a SOC analyst reads first. */
104
+ function message(row) {
105
+ const what = row.command?.program ?? row.subject ?? row.tool;
106
+ if (isEvent(row)) return `${row.rule ?? row.tool}: ${row.reason ?? what}`;
107
+ const verb = row.verdict === "deny" ? row.phase === "output" ? "ran; output withheld from the model" : "refused before execution" : row.verdict === "ask" ? "held for approval" : row.rule === void 0 ? "allowed" : "allowed and recorded";
108
+ const shadow = row.shadow ? " (shadow mode)" : "";
109
+ const rule = row.rule === void 0 ? "" : ` · ${row.rule}`;
110
+ return `${row.tool} ${what}: ${verb}${shadow}${rule}`;
111
+ }
112
+ function finding(row, options) {
113
+ const event = isEvent(row);
114
+ const severity = event ? SEVERITY.informational : row.verdict === "deny" ? SEVERITY.high : row.verdict === "ask" || row.verdict === "correct" ? SEVERITY.medium : SEVERITY.low;
115
+ const disposition = event ? "logged" : row.shadow ? "logged" : row.verdict === "deny" ? row.phase === "output" ? "withheld" : "blocked" : row.verdict === "ask" ? "held" : "logged";
116
+ const action = disposition === "blocked" || disposition === "withheld" ? ACTION.denied : ACTION.observed;
117
+ const activity = 1;
118
+ return {
119
+ class_uid: DETECTION_FINDING,
120
+ category_uid: 2,
121
+ activity_id: activity,
122
+ type_uid: DETECTION_FINDING * 100 + activity,
123
+ time: Date.parse(row.at),
124
+ severity_id: severity,
125
+ status_id: 1,
126
+ action_id: action,
127
+ is_alert: !event && row.verdict === "deny" && row.shadow !== true,
128
+ message: message(row),
129
+ metadata: metadata(row, options),
130
+ finding_info: {
131
+ uid: `${row.session ?? "ledger"}#${row.seq}`,
132
+ title: event ? `PRAE ${row.rule ?? row.tool}` : `PRAE ${row.verdict}${row.phase === "output" ? " (output)" : ""}${row.rule === void 0 ? "" : ` · ${row.rule}`}`,
133
+ ...row.reason === void 0 ? {} : { desc: row.reason },
134
+ analytic: {
135
+ name: row.rule ?? "baseline",
136
+ type_id: 1,
137
+ ...row.pack === void 0 ? {} : { version: `${row.pack.name} ${row.pack.version}` }
138
+ },
139
+ ...row.capability === void 0 ? {} : { types: [row.capability] }
140
+ },
141
+ evidences: evidences(row),
142
+ unmapped: unmapped(row, disposition)
143
+ };
144
+ }
145
+ function apiActivity(row, options) {
146
+ const activity = API_ACTIVITY_OF[row.tool] ?? 99;
147
+ return {
148
+ class_uid: API_ACTIVITY,
149
+ category_uid: 6,
150
+ activity_id: activity,
151
+ type_uid: API_ACTIVITY * 100 + activity,
152
+ time: Date.parse(row.at),
153
+ severity_id: SEVERITY.informational,
154
+ status_id: 1,
155
+ action_id: ACTION.allowed,
156
+ message: message(row),
157
+ metadata: metadata(row, options),
158
+ actor: {
159
+ app_name: "dsh",
160
+ ...row.session === void 0 ? {} : { session: { uid: row.session } }
161
+ },
162
+ api: {
163
+ operation: row.tool,
164
+ request: { uid: `${row.seq}` },
165
+ response: {
166
+ code: 0,
167
+ message: "allowed"
168
+ }
169
+ },
170
+ src_endpoint: { name: "agent harness" },
171
+ resources: resources(row),
172
+ unmapped: unmapped(row, "allowed")
173
+ };
174
+ }
175
+ /** One OCSF event per ledger row, in file order. */
176
+ function toOcsf(rows, options) {
177
+ return rows.map((row) => isEvent(row) || row.verdict !== "allow" || row.rule !== void 0 ? finding(row, options) : apiActivity(row, options));
178
+ }
179
+ /** Newline-delimited JSON, the form every collector takes. */
180
+ function ndjson(events) {
181
+ return `${events.map((event) => JSON.stringify(event)).join("\n")}\n`;
182
+ }
183
+ /**
184
+ * Splunk HTTP Event Collector: one request, one JSON object per line, each
185
+ * wrapping the event with its time. Sentinel and Security Lake take the
186
+ * NDJSON file through their own ingestion; only HEC has a wire this simple.
187
+ */
188
+ async function sendToHec(events, url, token, fetchImpl = fetch) {
189
+ const endpoint = `${url.replace(/\/$/, "")}/services/collector/event`;
190
+ const body = events.map((event) => JSON.stringify({
191
+ time: Number(event.time) / 1e3,
192
+ sourcetype: "ocsf:prae",
193
+ event
194
+ })).join("\n");
195
+ const response = await fetchImpl(endpoint, {
196
+ method: "POST",
197
+ headers: {
198
+ authorization: `Splunk ${token}`,
199
+ "content-type": "application/json"
200
+ },
201
+ body
202
+ });
203
+ return {
204
+ sent: events.length,
205
+ status: response.status,
206
+ body: await response.text()
207
+ };
208
+ }
209
+
210
+ //#endregion
211
+ export { OCSF_VERSION, ndjson, sendToHec, toOcsf };