prae-cli 0.7.1 → 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 +2 -2
- package/lib/checkpoint-DiznOVpS.js +99 -0
- package/lib/{cli-mahLCjoD.js → cli-UAE968If.js} +111 -20
- package/lib/index.js +2 -2
- package/lib/{instructions-C104EUaE.js → instructions-DulxSqFd.js} +1 -1
- package/lib/{mcp-DDcejhwQ.js → mcp-doNYU-1C.js} +1 -1
- package/lib/ocsf-DhVGbBUw.js +211 -0
- package/lib/playground.js +92 -44
- package/lib/prae-baseline.prae.json +17 -9
- package/lib/secret-catcher.prae.json +7 -4
- package/lib/{src-Vnr0fcpK.js → src-CwBEFhww.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/{test-run-DOaCKD1t.js → test-run-XNIRHKOu.js} +0 -0
package/lib/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./evaluate-wMPkjcIC.js";
|
|
3
|
-
import "./views-
|
|
4
|
-
import { t as main } from "./cli-
|
|
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
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-
|
|
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";
|
|
@@ -972,7 +972,6 @@ function verifyRecord(record, tamper, repair = false) {
|
|
|
972
972
|
signatures
|
|
973
973
|
};
|
|
974
974
|
}
|
|
975
|
-
const OWASP = "owasp-llm-top-10-2026";
|
|
976
975
|
/**
|
|
977
976
|
* Evidence toward each control, from the pack and the ledger as they are
|
|
978
977
|
* now — the rows the controls package defines, so the console, the exporter and
|
|
@@ -1001,15 +1000,16 @@ function complianceOf(record) {
|
|
|
1001
1000
|
verification: verifyEntries(text),
|
|
1002
1001
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1003
1002
|
};
|
|
1004
|
-
const owasp = FRAMEWORKS[OWASP]?.controls ?? {};
|
|
1005
1003
|
return { controls: CONTROLS.map((control) => {
|
|
1006
|
-
const
|
|
1007
|
-
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];
|
|
1008
1007
|
return {
|
|
1009
1008
|
id: code ?? control.id,
|
|
1010
1009
|
...named === void 0 ? {} : { name: named.name },
|
|
1011
1010
|
asks: control.asks,
|
|
1012
|
-
framework:
|
|
1011
|
+
framework: found === void 0 ? "obligation" : found[0],
|
|
1012
|
+
...found === void 0 ? {} : { frameworkName: found[1].name },
|
|
1013
1013
|
...control.evidence(data)
|
|
1014
1014
|
};
|
|
1015
1015
|
}) };
|
|
@@ -1187,9 +1187,23 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1187
1187
|
the exit code is non-zero if any invariant no longer holds, so a
|
|
1188
1188
|
control pack can be defended in CI. --pack overrides the suite's own.
|
|
1189
1189
|
|
|
1190
|
-
prae verify <ledger.jsonl> [--key <public.pem>]
|
|
1190
|
+
prae verify <ledger.jsonl> [--key <public.pem>] [--checkpoint <file>]
|
|
1191
1191
|
Re-derive every hash in a decision log and report any break. With
|
|
1192
|
-
--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.
|
|
1193
1207
|
|
|
1194
1208
|
prae keygen [--out <dir>]
|
|
1195
1209
|
Generate an Ed25519 keypair for signing ledger rows. Prints the
|
|
@@ -1270,7 +1284,7 @@ async function check(path) {
|
|
|
1270
1284
|
* from the findings, so there is nothing here to be careful about printing.
|
|
1271
1285
|
*/
|
|
1272
1286
|
async function scanFile(path, packPath, json) {
|
|
1273
|
-
const { scan } = await import("./src-
|
|
1287
|
+
const { scan } = await import("./src-CwBEFhww.js");
|
|
1274
1288
|
let content;
|
|
1275
1289
|
try {
|
|
1276
1290
|
content = await readFile(path, "utf8");
|
|
@@ -1310,7 +1324,7 @@ async function scanFile(path, packPath, json) {
|
|
|
1310
1324
|
* not come up inspecting nothing.
|
|
1311
1325
|
*/
|
|
1312
1326
|
async function mcp(root, packPath, ledgerPath) {
|
|
1313
|
-
const { serveMcp } = await import("./mcp-
|
|
1327
|
+
const { serveMcp } = await import("./mcp-doNYU-1C.js");
|
|
1314
1328
|
const { resolve: resolve$1 } = await import("node:path");
|
|
1315
1329
|
const bundled = [join(import.meta.dirname, "secret-catcher.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "secret-catcher.prae.json")].find((candidate) => existsSync(candidate));
|
|
1316
1330
|
const chosen = packPath ?? bundled;
|
|
@@ -1414,7 +1428,7 @@ async function receipt(path, seq, keyPath, out) {
|
|
|
1414
1428
|
* file and sets the exit code, so scripts can branch on it.
|
|
1415
1429
|
*/
|
|
1416
1430
|
async function verifyReceiptCmd(path) {
|
|
1417
|
-
const { checkReceipt } = await import("./receipt-check-
|
|
1431
|
+
const { checkReceipt } = await import("./receipt-check-s3Aryyz-.js");
|
|
1418
1432
|
let parsed;
|
|
1419
1433
|
try {
|
|
1420
1434
|
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
@@ -1445,7 +1459,7 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
1445
1459
|
} catch (error) {
|
|
1446
1460
|
return fail(`${suitePath} is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
1447
1461
|
}
|
|
1448
|
-
const { parseSuite, renderSuite, runSuite } = await import("./test-run-
|
|
1462
|
+
const { parseSuite, renderSuite, runSuite } = await import("./test-run-XNIRHKOu.js");
|
|
1449
1463
|
const suite = parseSuite(parsed);
|
|
1450
1464
|
if (!suite.ok) {
|
|
1451
1465
|
console.error(`prae: ${suitePath} is not a valid test suite`);
|
|
@@ -1456,7 +1470,7 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
1456
1470
|
if (chosen === void 0) return fail("test needs a pack: set `pack` in the suite or pass --pack");
|
|
1457
1471
|
const pack = await loadPack(chosen);
|
|
1458
1472
|
const { capabilityForTool } = await import("capmark");
|
|
1459
|
-
const { scan } = await import("./src-
|
|
1473
|
+
const { scan } = await import("./src-CwBEFhww.js");
|
|
1460
1474
|
const inspect = pack.rules.some((rule) => rule.when.secrets !== void 0) ? (path) => {
|
|
1461
1475
|
try {
|
|
1462
1476
|
return scan(readFileSync(path, "utf8"));
|
|
@@ -1472,7 +1486,7 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
1472
1486
|
else for (const line of renderSuite(result)) console.log(line);
|
|
1473
1487
|
if (!result.ok) process.exit(1);
|
|
1474
1488
|
}
|
|
1475
|
-
async function verify(path, keyPath) {
|
|
1489
|
+
async function verify(path, keyPath, checkpointPath) {
|
|
1476
1490
|
let ledger;
|
|
1477
1491
|
try {
|
|
1478
1492
|
ledger = Ledger.open(path);
|
|
@@ -1484,6 +1498,13 @@ async function verify(path, keyPath) {
|
|
|
1484
1498
|
const entries = ledger.read();
|
|
1485
1499
|
const last = entries.at(-1);
|
|
1486
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
|
+
}
|
|
1487
1508
|
if (keyPath !== void 0) {
|
|
1488
1509
|
const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
|
|
1489
1510
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
@@ -1563,7 +1584,7 @@ async function report(path, out, packPath) {
|
|
|
1563
1584
|
* that step is a proposal a human approves, and it starts from this list.
|
|
1564
1585
|
*/
|
|
1565
1586
|
async function instructions(from, asJson) {
|
|
1566
|
-
const { ingest } = await import("./src-
|
|
1587
|
+
const { ingest } = await import("./src-d4c_n6wd.js");
|
|
1567
1588
|
const result = ingest(from);
|
|
1568
1589
|
if (asJson) {
|
|
1569
1590
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -1599,7 +1620,7 @@ async function instructions(from, asJson) {
|
|
|
1599
1620
|
* on purpose rather than something that happened to their policy.
|
|
1600
1621
|
*/
|
|
1601
1622
|
async function propose(from, options) {
|
|
1602
|
-
const [{ ingest }, { extract }] = await Promise.all([import("./src-
|
|
1623
|
+
const [{ ingest }, { extract }] = await Promise.all([import("./src-d4c_n6wd.js"), import("./src-CBGZVvt7.js")]);
|
|
1603
1624
|
const read = ingest(from);
|
|
1604
1625
|
if (read.files.length === 0) {
|
|
1605
1626
|
console.log(`No instruction files found at or above ${from}.`);
|
|
@@ -1659,9 +1680,9 @@ async function consoleOut(path, argv) {
|
|
|
1659
1680
|
const instructionsDir = flag(argv, "instructions");
|
|
1660
1681
|
if (instructionsDir !== void 0) {
|
|
1661
1682
|
const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
|
|
1662
|
-
import("./src-
|
|
1663
|
-
import("./src-
|
|
1664
|
-
import("./instructions-
|
|
1683
|
+
import("./src-d4c_n6wd.js"),
|
|
1684
|
+
import("./src-CBGZVvt7.js"),
|
|
1685
|
+
import("./instructions-DulxSqFd.js")
|
|
1665
1686
|
]);
|
|
1666
1687
|
const read = ingest(instructionsDir);
|
|
1667
1688
|
instructionsView = renderInstructions(data, read, extract(read.statements));
|
|
@@ -1759,6 +1780,60 @@ async function init(argv) {
|
|
|
1759
1780
|
return fail(error instanceof Error ? error.message : String(error));
|
|
1760
1781
|
}
|
|
1761
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
|
+
}
|
|
1762
1837
|
async function main(argv) {
|
|
1763
1838
|
const [command, ...rest] = argv;
|
|
1764
1839
|
switch (command) {
|
|
@@ -1798,7 +1873,23 @@ async function main(argv) {
|
|
|
1798
1873
|
case "verify": {
|
|
1799
1874
|
const path = rest[0];
|
|
1800
1875
|
if (path === void 0) fail("verify needs a ledger file");
|
|
1801
|
-
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
|
+
});
|
|
1802
1893
|
}
|
|
1803
1894
|
case "verify-receipt": {
|
|
1804
1895
|
const path = rest[0];
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./evaluate-wMPkjcIC.js";
|
|
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-
|
|
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
1
|
import { t as evaluate } from "./evaluate-wMPkjcIC.js";
|
|
2
|
-
import { a as scan, n as MAX_SCAN_BYTES } from "./src-
|
|
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 };
|
package/lib/playground.js
CHANGED
|
@@ -491,52 +491,100 @@
|
|
|
491
491
|
|
|
492
492
|
//#endregion
|
|
493
493
|
//#region ../kernel/src/frameworks.ts
|
|
494
|
-
const FRAMEWORKS = {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
494
|
+
const FRAMEWORKS = {
|
|
495
|
+
"owasp-llm-top-10-2026": {
|
|
496
|
+
name: "OWASP Top 10 for LLM Applications — 2026",
|
|
497
|
+
short: "OWASP LLM 2026",
|
|
498
|
+
controls: {
|
|
499
|
+
LLM01: {
|
|
500
|
+
name: "Prompt Injection",
|
|
501
|
+
summary: "Instructions in a prompt or in retrieved content alter what the model does."
|
|
502
|
+
},
|
|
503
|
+
LLM02: {
|
|
504
|
+
name: "Sensitive Information Disclosure",
|
|
505
|
+
summary: "Credentials, personal data or proprietary content reach the model or its output."
|
|
506
|
+
},
|
|
507
|
+
LLM03: {
|
|
508
|
+
name: "Excessive Agency",
|
|
509
|
+
summary: "The model can take actions, or holds permissions, beyond what its task needs."
|
|
510
|
+
},
|
|
511
|
+
LLM04: {
|
|
512
|
+
name: "Supply Chain",
|
|
513
|
+
summary: "A compromised model, dataset, plugin or dependency enters the application."
|
|
514
|
+
},
|
|
515
|
+
LLM05: {
|
|
516
|
+
name: "Data and Model Poisoning",
|
|
517
|
+
summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
|
|
518
|
+
},
|
|
519
|
+
LLM06: {
|
|
520
|
+
name: "Unbounded Consumption",
|
|
521
|
+
summary: "The application permits resource use without a limit, exhausting budget or service."
|
|
522
|
+
},
|
|
523
|
+
LLM07: {
|
|
524
|
+
name: "Misinformation",
|
|
525
|
+
summary: "The model produces false or misleading content that is acted on as if true."
|
|
526
|
+
},
|
|
527
|
+
LLM08: {
|
|
528
|
+
name: "Hidden Context Exposure",
|
|
529
|
+
summary: "System prompts, hidden instructions or internal context are revealed."
|
|
530
|
+
},
|
|
531
|
+
LLM09: {
|
|
532
|
+
name: "Vector and Embedding Weaknesses",
|
|
533
|
+
summary: "Retrieval stores are manipulated or leak across tenants and permissions."
|
|
534
|
+
},
|
|
535
|
+
LLM10: {
|
|
536
|
+
name: "Improper Output Handling",
|
|
537
|
+
summary: "Model output is passed to downstream systems without validation."
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
"owasp-agentic-top-10-2026": {
|
|
542
|
+
name: "OWASP Top 10 for Agentic Applications — 2026",
|
|
543
|
+
short: "OWASP Agentic 2026",
|
|
544
|
+
controls: {
|
|
545
|
+
ASI01: {
|
|
546
|
+
name: "Agent Goal Hijack",
|
|
547
|
+
summary: "Untrusted input redirects what the agent is trying to do, turning its objectives against its owner."
|
|
548
|
+
},
|
|
549
|
+
ASI02: {
|
|
550
|
+
name: "Tool Misuse and Exploitation",
|
|
551
|
+
summary: "A legitimate tool the agent already holds is turned to an attacker's ends."
|
|
552
|
+
},
|
|
553
|
+
ASI03: {
|
|
554
|
+
name: "Identity and Privilege Abuse",
|
|
555
|
+
summary: "The agent's own authority, credentials or delegated access is inherited or escalated."
|
|
556
|
+
},
|
|
557
|
+
ASI04: {
|
|
558
|
+
name: "Agentic Supply Chain",
|
|
559
|
+
summary: "A poisoned tool, model, plugin, skill or dependency is trusted before the agent runs."
|
|
560
|
+
},
|
|
561
|
+
ASI05: {
|
|
562
|
+
name: "Unexpected Code Execution",
|
|
563
|
+
summary: "The agent's output reaches something that runs it: a shell, an interpreter, a parser."
|
|
564
|
+
},
|
|
565
|
+
ASI06: {
|
|
566
|
+
name: "Memory and Context Poisoning",
|
|
567
|
+
summary: "Corrupted memory or retrieved context persists across sessions and steers future reasoning."
|
|
568
|
+
},
|
|
569
|
+
ASI07: {
|
|
570
|
+
name: "Insecure Inter-Agent Communication",
|
|
571
|
+
summary: "One agent trusts another's messages by default, so a compromised agent can steer its peers."
|
|
572
|
+
},
|
|
573
|
+
ASI08: {
|
|
574
|
+
name: "Cascading Failures",
|
|
575
|
+
summary: "A single fault propagates across agents, tools and workflows into system-wide impact."
|
|
576
|
+
},
|
|
577
|
+
ASI09: {
|
|
578
|
+
name: "Human-Agent Trust Exploitation",
|
|
579
|
+
summary: "Persuasive or misleading agent output moves a person to an unsafe action or approval."
|
|
580
|
+
},
|
|
581
|
+
ASI10: {
|
|
582
|
+
name: "Rogue Agents",
|
|
583
|
+
summary: "A compromised, misaligned or drifting agent keeps operating in unintended ways."
|
|
584
|
+
}
|
|
537
585
|
}
|
|
538
586
|
}
|
|
539
|
-
}
|
|
587
|
+
};
|
|
540
588
|
|
|
541
589
|
//#endregion
|
|
542
590
|
//#region ../kernel/src/pack.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"prae": "0.1",
|
|
3
3
|
"pack": "prae-baseline",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"rules": [
|
|
6
6
|
{
|
|
7
7
|
"id": "BASE-001",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"because": "piping a download into an interpreter runs code nobody has read"
|
|
18
18
|
},
|
|
19
19
|
"frameworks": {
|
|
20
|
-
"owasp-llm-top-10-2026": ["LLM03"]
|
|
20
|
+
"owasp-llm-top-10-2026": ["LLM03"],
|
|
21
|
+
"owasp-agentic-top-10-2026": ["ASI05", "ASI02"]
|
|
21
22
|
}
|
|
22
23
|
},
|
|
23
24
|
{
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
"because": "a recursive force-delete is not reversible and needs a human"
|
|
36
37
|
},
|
|
37
38
|
"frameworks": {
|
|
38
|
-
"owasp-llm-top-10-2026": ["LLM03"]
|
|
39
|
+
"owasp-llm-top-10-2026": ["LLM03"],
|
|
40
|
+
"owasp-agentic-top-10-2026": ["ASI02"]
|
|
39
41
|
}
|
|
40
42
|
},
|
|
41
43
|
{
|
|
@@ -51,7 +53,8 @@
|
|
|
51
53
|
"because": "escalating privileges needs a human"
|
|
52
54
|
},
|
|
53
55
|
"frameworks": {
|
|
54
|
-
"owasp-llm-top-10-2026": ["LLM03"]
|
|
56
|
+
"owasp-llm-top-10-2026": ["LLM03"],
|
|
57
|
+
"owasp-agentic-top-10-2026": ["ASI03"]
|
|
55
58
|
}
|
|
56
59
|
},
|
|
57
60
|
{
|
|
@@ -73,7 +76,8 @@
|
|
|
73
76
|
"because": "block-device and filesystem tools are not granted to agents"
|
|
74
77
|
},
|
|
75
78
|
"frameworks": {
|
|
76
|
-
"owasp-llm-top-10-2026": ["LLM03"]
|
|
79
|
+
"owasp-llm-top-10-2026": ["LLM03"],
|
|
80
|
+
"owasp-agentic-top-10-2026": ["ASI02"]
|
|
77
81
|
}
|
|
78
82
|
},
|
|
79
83
|
{
|
|
@@ -98,7 +102,8 @@
|
|
|
98
102
|
"because": "secrets and credential stores are out of scope for agents"
|
|
99
103
|
},
|
|
100
104
|
"frameworks": {
|
|
101
|
-
"owasp-llm-top-10-2026": ["LLM02"]
|
|
105
|
+
"owasp-llm-top-10-2026": ["LLM02"],
|
|
106
|
+
"owasp-agentic-top-10-2026": ["ASI03"]
|
|
102
107
|
}
|
|
103
108
|
},
|
|
104
109
|
{
|
|
@@ -115,7 +120,8 @@
|
|
|
115
120
|
"because": "writing outside the working tree needs a human"
|
|
116
121
|
},
|
|
117
122
|
"frameworks": {
|
|
118
|
-
"owasp-llm-top-10-2026": ["LLM03"]
|
|
123
|
+
"owasp-llm-top-10-2026": ["LLM03"],
|
|
124
|
+
"owasp-agentic-top-10-2026": ["ASI02"]
|
|
119
125
|
}
|
|
120
126
|
},
|
|
121
127
|
{
|
|
@@ -132,7 +138,8 @@
|
|
|
132
138
|
"because": "rewriting shared history needs a human"
|
|
133
139
|
},
|
|
134
140
|
"frameworks": {
|
|
135
|
-
"owasp-llm-top-10-2026": ["LLM03"]
|
|
141
|
+
"owasp-llm-top-10-2026": ["LLM03"],
|
|
142
|
+
"owasp-agentic-top-10-2026": ["ASI02"]
|
|
136
143
|
}
|
|
137
144
|
},
|
|
138
145
|
{
|
|
@@ -149,7 +156,8 @@
|
|
|
149
156
|
"because": "publishing is outward-facing and needs a human"
|
|
150
157
|
},
|
|
151
158
|
"frameworks": {
|
|
152
|
-
"owasp-llm-top-10-2026": ["LLM03"]
|
|
159
|
+
"owasp-llm-top-10-2026": ["LLM03"],
|
|
160
|
+
"owasp-agentic-top-10-2026": ["ASI02"]
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
163
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"prae": "0.1",
|
|
3
3
|
"pack": "secret-catcher",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"rules": [
|
|
6
6
|
{
|
|
7
7
|
"id": "SEC-001",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"because": "the requested file contains a protected credential; the contents were not returned"
|
|
27
27
|
},
|
|
28
28
|
"frameworks": {
|
|
29
|
-
"owasp-llm-top-10-2026": ["LLM02"]
|
|
29
|
+
"owasp-llm-top-10-2026": ["LLM02"],
|
|
30
|
+
"owasp-agentic-top-10-2026": ["ASI03"]
|
|
30
31
|
}
|
|
31
32
|
},
|
|
32
33
|
{
|
|
@@ -43,7 +44,8 @@
|
|
|
43
44
|
"because": "the requested file contains a private key; the contents were not returned"
|
|
44
45
|
},
|
|
45
46
|
"frameworks": {
|
|
46
|
-
"owasp-llm-top-10-2026": ["LLM02"]
|
|
47
|
+
"owasp-llm-top-10-2026": ["LLM02"],
|
|
48
|
+
"owasp-agentic-top-10-2026": ["ASI03"]
|
|
47
49
|
}
|
|
48
50
|
},
|
|
49
51
|
{
|
|
@@ -74,7 +76,8 @@
|
|
|
74
76
|
"because": "the command ran and its output contained a protected credential; the output was withheld from the model"
|
|
75
77
|
},
|
|
76
78
|
"frameworks": {
|
|
77
|
-
"owasp-llm-top-10-2026": ["LLM02"]
|
|
79
|
+
"owasp-llm-top-10-2026": ["LLM02"],
|
|
80
|
+
"owasp-agentic-top-10-2026": ["ASI03"]
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
]
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as scan, i as looksBinary, n as MAX_SCAN_BYTES, r as entropy, t as DETECTORS } from "./src-
|
|
1
|
+
import { a as scan, i as looksBinary, n as MAX_SCAN_BYTES, r as entropy, t as DETECTORS } from "./src-B25SHWcz.js";
|
|
2
2
|
|
|
3
3
|
export { DETECTORS, MAX_SCAN_BYTES, entropy, looksBinary, scan };
|
|
@@ -1,50 +1,98 @@
|
|
|
1
1
|
//#region ../kernel/src/frameworks.ts
|
|
2
|
-
const FRAMEWORKS = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
2
|
+
const FRAMEWORKS = {
|
|
3
|
+
"owasp-llm-top-10-2026": {
|
|
4
|
+
name: "OWASP Top 10 for LLM Applications — 2026",
|
|
5
|
+
short: "OWASP LLM 2026",
|
|
6
|
+
controls: {
|
|
7
|
+
LLM01: {
|
|
8
|
+
name: "Prompt Injection",
|
|
9
|
+
summary: "Instructions in a prompt or in retrieved content alter what the model does."
|
|
10
|
+
},
|
|
11
|
+
LLM02: {
|
|
12
|
+
name: "Sensitive Information Disclosure",
|
|
13
|
+
summary: "Credentials, personal data or proprietary content reach the model or its output."
|
|
14
|
+
},
|
|
15
|
+
LLM03: {
|
|
16
|
+
name: "Excessive Agency",
|
|
17
|
+
summary: "The model can take actions, or holds permissions, beyond what its task needs."
|
|
18
|
+
},
|
|
19
|
+
LLM04: {
|
|
20
|
+
name: "Supply Chain",
|
|
21
|
+
summary: "A compromised model, dataset, plugin or dependency enters the application."
|
|
22
|
+
},
|
|
23
|
+
LLM05: {
|
|
24
|
+
name: "Data and Model Poisoning",
|
|
25
|
+
summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
|
|
26
|
+
},
|
|
27
|
+
LLM06: {
|
|
28
|
+
name: "Unbounded Consumption",
|
|
29
|
+
summary: "The application permits resource use without a limit, exhausting budget or service."
|
|
30
|
+
},
|
|
31
|
+
LLM07: {
|
|
32
|
+
name: "Misinformation",
|
|
33
|
+
summary: "The model produces false or misleading content that is acted on as if true."
|
|
34
|
+
},
|
|
35
|
+
LLM08: {
|
|
36
|
+
name: "Hidden Context Exposure",
|
|
37
|
+
summary: "System prompts, hidden instructions or internal context are revealed."
|
|
38
|
+
},
|
|
39
|
+
LLM09: {
|
|
40
|
+
name: "Vector and Embedding Weaknesses",
|
|
41
|
+
summary: "Retrieval stores are manipulated or leak across tenants and permissions."
|
|
42
|
+
},
|
|
43
|
+
LLM10: {
|
|
44
|
+
name: "Improper Output Handling",
|
|
45
|
+
summary: "Model output is passed to downstream systems without validation."
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"owasp-agentic-top-10-2026": {
|
|
50
|
+
name: "OWASP Top 10 for Agentic Applications — 2026",
|
|
51
|
+
short: "OWASP Agentic 2026",
|
|
52
|
+
controls: {
|
|
53
|
+
ASI01: {
|
|
54
|
+
name: "Agent Goal Hijack",
|
|
55
|
+
summary: "Untrusted input redirects what the agent is trying to do, turning its objectives against its owner."
|
|
56
|
+
},
|
|
57
|
+
ASI02: {
|
|
58
|
+
name: "Tool Misuse and Exploitation",
|
|
59
|
+
summary: "A legitimate tool the agent already holds is turned to an attacker's ends."
|
|
60
|
+
},
|
|
61
|
+
ASI03: {
|
|
62
|
+
name: "Identity and Privilege Abuse",
|
|
63
|
+
summary: "The agent's own authority, credentials or delegated access is inherited or escalated."
|
|
64
|
+
},
|
|
65
|
+
ASI04: {
|
|
66
|
+
name: "Agentic Supply Chain",
|
|
67
|
+
summary: "A poisoned tool, model, plugin, skill or dependency is trusted before the agent runs."
|
|
68
|
+
},
|
|
69
|
+
ASI05: {
|
|
70
|
+
name: "Unexpected Code Execution",
|
|
71
|
+
summary: "The agent's output reaches something that runs it: a shell, an interpreter, a parser."
|
|
72
|
+
},
|
|
73
|
+
ASI06: {
|
|
74
|
+
name: "Memory and Context Poisoning",
|
|
75
|
+
summary: "Corrupted memory or retrieved context persists across sessions and steers future reasoning."
|
|
76
|
+
},
|
|
77
|
+
ASI07: {
|
|
78
|
+
name: "Insecure Inter-Agent Communication",
|
|
79
|
+
summary: "One agent trusts another's messages by default, so a compromised agent can steer its peers."
|
|
80
|
+
},
|
|
81
|
+
ASI08: {
|
|
82
|
+
name: "Cascading Failures",
|
|
83
|
+
summary: "A single fault propagates across agents, tools and workflows into system-wide impact."
|
|
84
|
+
},
|
|
85
|
+
ASI09: {
|
|
86
|
+
name: "Human-Agent Trust Exploitation",
|
|
87
|
+
summary: "Persuasive or misleading agent output moves a person to an unsafe action or approval."
|
|
88
|
+
},
|
|
89
|
+
ASI10: {
|
|
90
|
+
name: "Rogue Agents",
|
|
91
|
+
summary: "A compromised, misaligned or drifting agent keeps operating in unintended ways."
|
|
92
|
+
}
|
|
45
93
|
}
|
|
46
94
|
}
|
|
47
|
-
}
|
|
95
|
+
};
|
|
48
96
|
/**
|
|
49
97
|
* Every framework control a rule cites, resolved to names.
|
|
50
98
|
*
|
|
@@ -802,6 +850,7 @@ const OBLIGATIONS = [
|
|
|
802
850
|
}
|
|
803
851
|
];
|
|
804
852
|
const OWASP = "owasp-llm-top-10-2026";
|
|
853
|
+
const AGENTIC = "owasp-agentic-top-10-2026";
|
|
805
854
|
/** Rules in the pack that cite one control of one framework. */
|
|
806
855
|
function citing(pack, framework, control) {
|
|
807
856
|
return (pack?.rules ?? []).filter((rule) => (rule.frameworks?.[framework] ?? []).includes(control));
|
|
@@ -819,32 +868,91 @@ const NOT_THIS_BOUNDARY = {
|
|
|
819
868
|
LLM07: "Whether an answer is true is not a property of a tool call.",
|
|
820
869
|
LLM08: "The assembled system prompt is sealed into the ledger as evidence of what the model was told; nothing here stops the model repeating it.",
|
|
821
870
|
LLM09: "Retrieval stores are upstream of the call the gate judges.",
|
|
822
|
-
LLM10: "What downstream code does with model output is that code's boundary, not this one's."
|
|
871
|
+
LLM10: "What downstream code does with model output is that code's boundary, not this one's.",
|
|
872
|
+
ASI04: "Where a tool, skill or plugin came from is settled before the agent runs. A rule on the skill tool (integrity, source) is how a pack refuses to trust one at load time; none in this pack does.",
|
|
873
|
+
ASI06: "Memory and retrieval stores are upstream of the call the gate judges. What the gate can do is seal what the agent was told (PRAE-ASM, PRAE-CTX) so a poisoned context is evidenced, never silent; this record holds no such seal.",
|
|
874
|
+
ASI07: "A message between agents is not a tool call; when it becomes one, it is judged like any other agent's call. The channel itself is outside this boundary.",
|
|
875
|
+
ASI09: "Whether a person was misled is not a property of a tool call. An ask verdict puts a human in the path and seals the outcome; nothing here judges the persuasion."
|
|
876
|
+
};
|
|
877
|
+
/**
|
|
878
|
+
* A risk this gate answers by *not* answering it: nothing here detects an
|
|
879
|
+
* injected instruction or a hijacked goal, and saying otherwise would be the
|
|
880
|
+
* claim a red team disproves first. What the gate does is bound what the
|
|
881
|
+
* subverted agent can execute — the cited controls of another risk doing
|
|
882
|
+
* their job. Said in those words, every time.
|
|
883
|
+
*/
|
|
884
|
+
const BOUNDED_BY = {
|
|
885
|
+
LLM01: {
|
|
886
|
+
framework: OWASP,
|
|
887
|
+
control: "LLM03"
|
|
888
|
+
},
|
|
889
|
+
ASI01: {
|
|
890
|
+
framework: AGENTIC,
|
|
891
|
+
control: "ASI02"
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
function bounded(data, risk, by) {
|
|
895
|
+
const bounding = citing(data.pack, by.framework, by.control);
|
|
896
|
+
const what = risk === "ASI01" ? "a hijacked goal" : "injected instructions";
|
|
897
|
+
const who = risk === "ASI01" ? "a hijacked agent" : "an injected instruction";
|
|
898
|
+
return {
|
|
899
|
+
state: "partial",
|
|
900
|
+
note: bounding.length === 0 ? `Not mitigated: this gate does not detect ${what}, and no ${by.control} control in this pack bounds what ${who} can execute.` : `Not mitigated: this gate does not detect ${what}. Bounded: ${bounding.length} ${by.control} control${bounding.length === 1 ? "" : "s"} (${bounding.map((r) => r.id).join(", ")}) decide what ${who} can execute.`
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Risks whose evidence is in the record rather than in a citing rule.
|
|
905
|
+
*
|
|
906
|
+
* ASI06: a poisoned context cannot be prevented here, but every change to what
|
|
907
|
+
* the agent was told is sealed, so it is evidenced. ASI10: a drifting agent's
|
|
908
|
+
* actions are judged like any other's; what the record shows is whether the
|
|
909
|
+
* gate refused any of them.
|
|
910
|
+
*/
|
|
911
|
+
const FROM_THE_RECORD = {
|
|
912
|
+
ASI06: (data) => {
|
|
913
|
+
const seals = data.entries.filter((e) => e.rule === "PRAE-ASM" || e.rule === "PRAE-CTX");
|
|
914
|
+
return seals.length === 0 ? void 0 : {
|
|
915
|
+
state: "partial",
|
|
916
|
+
note: `Evidenced, not prevented: ${seals.length} seal${seals.length === 1 ? "" : "s"} of what the agent was told (PRAE-ASM / PRAE-CTX) in this record; a poisoned context changes the hash and is answerable afterwards.`
|
|
917
|
+
};
|
|
918
|
+
},
|
|
919
|
+
ASI10: (data) => {
|
|
920
|
+
const refused = data.entries.filter((e) => e.verdict === "deny").length;
|
|
921
|
+
const armed = [
|
|
922
|
+
"ASI02",
|
|
923
|
+
"ASI03",
|
|
924
|
+
"ASI08"
|
|
925
|
+
].flatMap((c) => citing(data.pack, AGENTIC, c));
|
|
926
|
+
if (armed.length === 0) return void 0;
|
|
927
|
+
return {
|
|
928
|
+
state: refused > 0 ? "covered" : "partial",
|
|
929
|
+
note: refused > 0 ? `A drifting agent's actions are judged like any other's: ${armed.length} controls under ASI02/03/08 apply to every call, and ${refused} refusal${refused === 1 ? "" : "s"} in this record show them holding.` : `A drifting agent's actions are judged like any other's: ${armed.length} controls under ASI02/03/08 apply to every call; none has refused one in this record yet. Detecting drift itself is derived from the record, not a rule.`
|
|
930
|
+
};
|
|
931
|
+
}
|
|
823
932
|
};
|
|
824
|
-
function
|
|
825
|
-
const framework = FRAMEWORKS[
|
|
933
|
+
function frameworkRow(frameworkId, control) {
|
|
934
|
+
const framework = FRAMEWORKS[frameworkId];
|
|
826
935
|
const risk = framework?.controls[control];
|
|
827
936
|
const name = risk?.name ?? control;
|
|
828
937
|
return {
|
|
829
|
-
id: `${framework?.short ??
|
|
938
|
+
id: `${framework?.short ?? frameworkId} · ${control}`,
|
|
830
939
|
asks: `${name}. ${risk?.summary ?? ""}`.trim(),
|
|
831
940
|
evidence: (data) => {
|
|
832
941
|
if (data.pack === void 0) return {
|
|
833
942
|
state: "absent",
|
|
834
943
|
note: "No pack supplied to this view."
|
|
835
944
|
};
|
|
836
|
-
|
|
837
|
-
|
|
945
|
+
const by = BOUNDED_BY[control];
|
|
946
|
+
if (by !== void 0) return bounded(data, control, by);
|
|
947
|
+
const rules = citing(data.pack, frameworkId, control);
|
|
948
|
+
if (rules.length === 0) {
|
|
949
|
+
const derived = FROM_THE_RECORD[control]?.(data);
|
|
950
|
+
if (derived !== void 0) return derived;
|
|
838
951
|
return {
|
|
839
|
-
state: "
|
|
840
|
-
note:
|
|
952
|
+
state: "absent",
|
|
953
|
+
note: NOT_THIS_BOUNDARY[control] ?? "No rule in this pack cites this risk."
|
|
841
954
|
};
|
|
842
955
|
}
|
|
843
|
-
const rules = citing(data.pack, OWASP, control);
|
|
844
|
-
if (rules.length === 0) return {
|
|
845
|
-
state: "absent",
|
|
846
|
-
note: NOT_THIS_BOUNDARY[control] ?? "No rule in this pack cites this risk."
|
|
847
|
-
};
|
|
848
956
|
const ids = new Set(rules.map((rule) => rule.id));
|
|
849
957
|
const decisions = data.entries.filter((entry) => entry.rule !== void 0 && ids.has(entry.rule));
|
|
850
958
|
const listed = rules.map((rule) => rule.id).join(", ");
|
|
@@ -858,14 +966,18 @@ function owaspRow(control) {
|
|
|
858
966
|
}
|
|
859
967
|
};
|
|
860
968
|
}
|
|
861
|
-
const
|
|
969
|
+
const rowsOf = (frameworkId) => Object.keys(FRAMEWORKS[frameworkId]?.controls ?? {}).map((control) => frameworkRow(frameworkId, control));
|
|
862
970
|
/**
|
|
863
|
-
* The obligations first, then the
|
|
864
|
-
* on purpose: an auditor reads down one list and finds the
|
|
865
|
-
* the ledger evidences and the application risks the pack's
|
|
866
|
-
* each stated in its framework's own words.
|
|
971
|
+
* The obligations first, then the risks framework by framework. Two kinds of
|
|
972
|
+
* row in one table on purpose: an auditor reads down one list and finds the
|
|
973
|
+
* audit obligations the ledger evidences and the application risks the pack's
|
|
974
|
+
* rules address, each stated in its framework's own words.
|
|
867
975
|
*/
|
|
868
|
-
const CONTROLS = [
|
|
976
|
+
const CONTROLS = [
|
|
977
|
+
...OBLIGATIONS,
|
|
978
|
+
...rowsOf(OWASP),
|
|
979
|
+
...rowsOf(AGENTIC)
|
|
980
|
+
];
|
|
869
981
|
|
|
870
982
|
//#endregion
|
|
871
983
|
//#region src/console/views.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prae-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Operator policy for agent harnesses: validate packs, explain a decision, test a pack against security invariants, verify a decision log, and provision a governed DSH profile.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"vitest": "^3.2.4",
|
|
43
43
|
"@prae/extract": "0.1.0",
|
|
44
44
|
"@prae/inspect": "0.1.0",
|
|
45
|
+
"@prae/controls": "0.1.0",
|
|
45
46
|
"@prae/instructions": "0.1.0",
|
|
46
47
|
"@prae/kernel": "0.1.0",
|
|
47
48
|
"@prae/ledger": "0.1.0",
|
|
48
|
-
"@prae/controls": "0.1.0",
|
|
49
49
|
"@prae/tokens": "0.1.0"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|