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 +3 -3
- package/lib/checkpoint-DiznOVpS.js +99 -0
- package/lib/{cli-chtNj_el.js → cli-UAE968If.js} +124 -23
- package/lib/{evaluate-CkYySl7T.js → evaluate-wMPkjcIC.js} +26 -3
- package/lib/index.js +3 -3
- package/lib/{instructions-C104EUaE.js → instructions-DulxSqFd.js} +1 -1
- package/lib/{mcp-Bq0DNPPR.js → mcp-doNYU-1C.js} +2 -2
- package/lib/ocsf-DhVGbBUw.js +211 -0
- package/lib/playground.js +130 -49
- package/lib/prae-baseline.prae.json +68 -6
- package/lib/secret-catcher.prae.json +7 -4
- package/lib/{src-Vnr0fcpK.js → src-CwBEFhww.js} +1 -1
- package/lib/{test-run-04ubUcSb.js → test-run-XNIRHKOu.js} +1 -1
- package/lib/{views-UODslKwu.js → views-CCwVXpo2.js} +175 -63
- package/package.json +2 -2
- /package/lib/{receipt-check-DrDE3Wwi.js → receipt-check-s3Aryyz-.js} +0 -0
- /package/lib/{src-Br1tGPtS.js → src-B25SHWcz.js} +0 -0
- /package/lib/{src-CQ0UmFUK.js → src-CBGZVvt7.js} +0 -0
- /package/lib/{src-CraUX64d.js → src-d4c_n6wd.js} +0 -0
package/lib/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./evaluate-
|
|
3
|
-
import "./views-
|
|
4
|
-
import { t as main } from "./cli-
|
|
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-
|
|
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-
|
|
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
|
-
"
|
|
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 `
|
|
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
|
|
997
|
-
const
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1653
|
-
import("./src-
|
|
1654
|
-
import("./instructions-
|
|
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
|
|
140
|
-
} else if (args.length > 0) {
|
|
141
|
-
if (!parts.some(
|
|
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-
|
|
2
|
-
import "./views-
|
|
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-
|
|
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,5 +1,5 @@
|
|
|
1
|
-
import { t as evaluate } from "./evaluate-
|
|
2
|
-
import { a as scan, n as MAX_SCAN_BYTES } from "./src-
|
|
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 };
|