prae-cli 0.8.0 → 0.9.2
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 +4 -4
- package/lib/{checkpoint-DiznOVpS.js → checkpoint-C_q_Rpa5.js} +1 -1
- package/lib/{cli-UAE968If.js → cli-B_B6-bUZ.js} +1020 -51
- package/lib/{evaluate-wMPkjcIC.js → evaluate-BqLxhYiP.js} +10 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +4 -4
- package/lib/{instructions-DulxSqFd.js → instructions-ya0LVnZy.js} +1 -1
- package/lib/{mcp-doNYU-1C.js → mcp-BCXbpqu4.js} +1 -1
- package/lib/playground.js +23 -3
- package/lib/{receipt-check-s3Aryyz-.js → receipt-check-DQK_gVPc.js} +1 -1
- package/lib/src-7X6dO7gC.js +3 -0
- package/lib/src-LA7NdLKb.js +399 -0
- package/lib/{test-run-XNIRHKOu.js → test-run-CCDgJ0Dp.js} +1 -1
- package/lib/{views-CCwVXpo2.js → views-D4UEfbzt.js} +257 -1
- package/package.json +6 -5
- package/lib/src-C9L6NBvu.js +0 -3
- package/lib/src-Ciggx9ja.js +0 -242
|
@@ -1,9 +1,11 @@
|
|
|
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-
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync, readFileSync, readdirSync, statSync, watch } from "node:fs";
|
|
1
|
+
import { n as hostOf, r as withinAny, t as evaluate } from "./evaluate-BqLxhYiP.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-D4UEfbzt.js";
|
|
3
|
+
import { c as fingerprint, g as chain, m as GENESIS, n as readEntries, p as verifySignature, r as verifyEntries, s as verifyBytes, t as Ledger, v as seal } from "./src-LA7NdLKb.js";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
-
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
6
|
+
import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { parse } from "yaml";
|
|
7
9
|
import { homedir } from "node:os";
|
|
8
10
|
import { composePatchFile, preflightOps, revisionOf, takeSnapshot } from "dsh-overlay-check";
|
|
9
11
|
import { createServer } from "node:http";
|
|
@@ -275,9 +277,9 @@ function readEffect(check$1, path, value) {
|
|
|
275
277
|
check$1.fail(path, "expected an object");
|
|
276
278
|
return;
|
|
277
279
|
}
|
|
278
|
-
const kind = value.kind;
|
|
279
|
-
if (kind !== "deny" && kind !== "ask" && kind !== "correct" && kind !== "warn") {
|
|
280
|
-
check$1.fail(`${path}.kind`, "expected `deny`, `ask
|
|
280
|
+
const kind = value.kind === "hold" ? "ask" : value.kind;
|
|
281
|
+
if (kind !== "deny" && kind !== "ask" && kind !== "correct" && kind !== "warn" && kind !== "allow") {
|
|
282
|
+
check$1.fail(`${path}.kind`, "expected `allow`, `deny`, `hold` (or `ask`), `correct`, or `warn`");
|
|
281
283
|
return;
|
|
282
284
|
}
|
|
283
285
|
check$1.keys(path, value, kind === "correct" ? [
|
|
@@ -311,6 +313,7 @@ function readRule(check$1, path, value) {
|
|
|
311
313
|
check$1.keys(path, value, [
|
|
312
314
|
"id",
|
|
313
315
|
"title",
|
|
316
|
+
"description",
|
|
314
317
|
"when",
|
|
315
318
|
"effect",
|
|
316
319
|
"frameworks"
|
|
@@ -329,6 +332,10 @@ function readRule(check$1, path, value) {
|
|
|
329
332
|
const title = check$1.text(`${path}.title`, value.title);
|
|
330
333
|
if (title !== void 0) rule.title = title;
|
|
331
334
|
}
|
|
335
|
+
if (value.description !== void 0) {
|
|
336
|
+
const description = check$1.text(`${path}.description`, value.description);
|
|
337
|
+
if (description !== void 0) rule.description = description;
|
|
338
|
+
}
|
|
332
339
|
if (value.frameworks !== void 0) {
|
|
333
340
|
const frameworks = readFrameworks(check$1, `${path}.frameworks`, value.frameworks);
|
|
334
341
|
if (frameworks !== void 0) rule.frameworks = frameworks;
|
|
@@ -389,12 +396,16 @@ function parsePack(source) {
|
|
|
389
396
|
"prae",
|
|
390
397
|
"pack",
|
|
391
398
|
"version",
|
|
399
|
+
"description",
|
|
400
|
+
"owner",
|
|
392
401
|
"baseline",
|
|
393
402
|
"rules"
|
|
394
403
|
]);
|
|
395
404
|
if (raw.prae !== "0.1") check$1.fail(".prae", "expected `\"0.1\"`; this is the only pack version this build reads");
|
|
396
405
|
const name = check$1.text(".pack", raw.pack);
|
|
397
406
|
const version = check$1.text(".version", raw.version);
|
|
407
|
+
const description = raw.description === void 0 ? void 0 : check$1.text(".description", raw.description);
|
|
408
|
+
const owner = raw.owner === void 0 ? void 0 : check$1.text(".owner", raw.owner);
|
|
398
409
|
let baseline;
|
|
399
410
|
if (raw.baseline !== void 0) if (!isObject(raw.baseline)) check$1.fail(".baseline", "expected an object");
|
|
400
411
|
else {
|
|
@@ -431,12 +442,387 @@ function parsePack(source) {
|
|
|
431
442
|
prae: "0.1",
|
|
432
443
|
pack: name,
|
|
433
444
|
version,
|
|
445
|
+
...description === void 0 ? {} : { description },
|
|
446
|
+
...owner === void 0 ? {} : { owner },
|
|
434
447
|
...baseline === void 0 ? {} : { baseline },
|
|
435
448
|
rules
|
|
436
449
|
}
|
|
437
450
|
};
|
|
438
451
|
}
|
|
439
452
|
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region ../dsh/src/chain.ts
|
|
455
|
+
const STAGES = [
|
|
456
|
+
{
|
|
457
|
+
id: "untrusted-input",
|
|
458
|
+
step: 1,
|
|
459
|
+
name: "UNTRUSTED INPUT",
|
|
460
|
+
severity: "INFO",
|
|
461
|
+
definition: "The agent took in content from outside the trust boundary: a read of a path the classifier treats as customer-supplied, or a tool result screening found carrying instructions.",
|
|
462
|
+
rule: "fs:read of a path under the untrusted globs (default **/tickets/**), or a row whose screening outcome is steer or hold.",
|
|
463
|
+
owasp: ["ASI01"],
|
|
464
|
+
atlas: ["AML.T0051.001"],
|
|
465
|
+
alignment: "Where an indirect prompt injection (ATLAS AML.T0051.001) can enter; the read alone does not show that one did. Unless a screening row says so, nothing here detected an injection."
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: "credential-access",
|
|
469
|
+
step: 2,
|
|
470
|
+
name: "CREDENTIAL ACCESS",
|
|
471
|
+
severity: "HIGH",
|
|
472
|
+
definition: "The agent tried to reach a credential: a file that is one by its path, or content the inspector found holding one.",
|
|
473
|
+
rule: "The deciding or a matched rule has a secrets matcher, or the row carries content findings, or the target is under the credential globs (/etc/shadow, **/.ssh/**, **/*.pem, **/.env, …).",
|
|
474
|
+
owasp: ["ASI03"],
|
|
475
|
+
atlas: [
|
|
476
|
+
"AML.TA0013",
|
|
477
|
+
"AML.T0055",
|
|
478
|
+
"AML.T0098"
|
|
479
|
+
],
|
|
480
|
+
alignment: "ATLAS tactic Credential Access, with Unsecured Credentials (a credential in a file) and AI Agent Tool Credential Harvesting (an agent tool used to collect one)."
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: "exfiltration",
|
|
484
|
+
step: 3,
|
|
485
|
+
name: "EXFILTRATION ATTEMPT",
|
|
486
|
+
severity: "CRITICAL",
|
|
487
|
+
definition: "The agent tried to send a request to a destination outside the egress list: through the fetch tool, or a network client in the shell.",
|
|
488
|
+
rule: "A matched rule has a host matcher on a net:fetch call, or a command matcher naming a network client on a proc:spawn call, and the request is not stage 4.",
|
|
489
|
+
owasp: ["ASI02"],
|
|
490
|
+
atlas: ["AML.TA0010", "AML.T0086"],
|
|
491
|
+
alignment: "ATLAS Exfiltration via AI Agent Tool Invocation. The row shows an outbound request to an unapproved host was attempted; it does not show what data the request carried."
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
id: "token-abuse",
|
|
495
|
+
step: 4,
|
|
496
|
+
name: "TOKEN ABUSE",
|
|
497
|
+
severity: "CRITICAL",
|
|
498
|
+
definition: "A stage-3 request whose destination path is a token-issuance endpoint: an attempt to obtain or register a credential outside the trust boundary.",
|
|
499
|
+
rule: "Stage 3, and the URL path has a segment token, oauth, oauth2 or sts.",
|
|
500
|
+
owasp: ["ASI03"],
|
|
501
|
+
atlas: ["AML.TA0013", "AML.TA0010"],
|
|
502
|
+
alignment: "PRAE-specific: no ATLAS technique is this one-to-one. It is classified from the request; it is enforced by whichever egress rule refused it."
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: "runaway",
|
|
506
|
+
step: 5,
|
|
507
|
+
name: "RUNAWAY BEHAVIOUR",
|
|
508
|
+
severity: "HIGH",
|
|
509
|
+
definition: "The agent kept calling past a budget the pack sets for the session.",
|
|
510
|
+
rule: "The deciding or a matched rule has a count matcher.",
|
|
511
|
+
owasp: ["ASI08"],
|
|
512
|
+
atlas: ["AML.T0034.002"],
|
|
513
|
+
alignment: "ATLAS Agentic Resource Consumption. OWASP ASI08 (Cascading Failures) is the nearest Agentic entry, not an equivalent."
|
|
514
|
+
}
|
|
515
|
+
];
|
|
516
|
+
/** Paths whose content the classifier treats as from outside the boundary. */
|
|
517
|
+
const UNTRUSTED_GLOBS = ["**/tickets/**", "./tickets/**"];
|
|
518
|
+
/** Paths that are credentials by location, whatever they contain. */
|
|
519
|
+
const CREDENTIAL_GLOBS = [
|
|
520
|
+
"/etc/shadow",
|
|
521
|
+
"/etc/gshadow",
|
|
522
|
+
"/etc/sudoers",
|
|
523
|
+
"**/.ssh/**",
|
|
524
|
+
"**/.aws/**",
|
|
525
|
+
"**/.gnupg/**",
|
|
526
|
+
"**/*.pem",
|
|
527
|
+
"**/*.key",
|
|
528
|
+
"**/.env",
|
|
529
|
+
"**/.env.*",
|
|
530
|
+
"**/credentials/**",
|
|
531
|
+
"**/.netrc"
|
|
532
|
+
];
|
|
533
|
+
const NETWORK_CLIENTS = new Set([
|
|
534
|
+
"curl",
|
|
535
|
+
"wget",
|
|
536
|
+
"nc",
|
|
537
|
+
"ncat",
|
|
538
|
+
"netcat",
|
|
539
|
+
"ssh",
|
|
540
|
+
"scp",
|
|
541
|
+
"sftp",
|
|
542
|
+
"ftp",
|
|
543
|
+
"telnet",
|
|
544
|
+
"openssl"
|
|
545
|
+
]);
|
|
546
|
+
const TOKEN_SEGMENT = new Set([
|
|
547
|
+
"token",
|
|
548
|
+
"oauth",
|
|
549
|
+
"oauth2",
|
|
550
|
+
"sts"
|
|
551
|
+
]);
|
|
552
|
+
function rulesOf(row$1, pack) {
|
|
553
|
+
if (pack === void 0) return [];
|
|
554
|
+
const ids = [row$1.rule, ...(row$1.trace ?? []).map((t) => t.rule)].filter((id) => id !== void 0);
|
|
555
|
+
return [...new Set(ids)].map((id) => pack.rules.find((r) => r.id === id)).filter((r) => r !== void 0);
|
|
556
|
+
}
|
|
557
|
+
function pathOf(url) {
|
|
558
|
+
try {
|
|
559
|
+
return new URL(url).pathname.toLowerCase().split("/").filter(Boolean);
|
|
560
|
+
} catch {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const stage = (id) => STAGES.find((s) => s.id === id);
|
|
565
|
+
/**
|
|
566
|
+
* The stage of one row, or undefined when the row is not an attack-chain
|
|
567
|
+
* step. Reads only the sealed row and the pack; the same inputs give the
|
|
568
|
+
* same answer every time. Checked in order: budget, credential, egress,
|
|
569
|
+
* untrusted input. The first that holds decides.
|
|
570
|
+
*/
|
|
571
|
+
function classify(row$1, pack) {
|
|
572
|
+
if (row$1.tool.startsWith("prae:")) return void 0;
|
|
573
|
+
const rules = rulesOf(row$1, pack);
|
|
574
|
+
const target = row$1.subject ?? "";
|
|
575
|
+
const budget = rules.find((r) => r.when.count !== void 0);
|
|
576
|
+
if (budget !== void 0) return {
|
|
577
|
+
stage: stage("runaway"),
|
|
578
|
+
basis: `${budget.id} is a session budget (count above ${budget.when.count?.above})`
|
|
579
|
+
};
|
|
580
|
+
const secrets = rules.find((r) => r.when.secrets !== void 0);
|
|
581
|
+
if (secrets !== void 0 || (row$1.secrets?.length ?? 0) > 0) return {
|
|
582
|
+
stage: stage("credential-access"),
|
|
583
|
+
basis: secrets !== void 0 ? `${secrets.id} inspects content for credentials, and matched` : `content inspection found ${row$1.secrets?.length} credential finding(s)`
|
|
584
|
+
};
|
|
585
|
+
if (target !== "" && (row$1.capability === "fs:read" || row$1.capability === "fs:write" || row$1.capability === "proc:spawn") && withinAny(target, CREDENTIAL_GLOBS)) return {
|
|
586
|
+
stage: stage("credential-access"),
|
|
587
|
+
basis: `the target ${target} is a credential by its path`
|
|
588
|
+
};
|
|
589
|
+
const host = rules.find((r) => r.when.host !== void 0);
|
|
590
|
+
const shell = row$1.capability === "proc:spawn" && row$1.command !== void 0 && NETWORK_CLIENTS.has(row$1.command.program) ? rules.find((r) => r.when.command !== void 0) : void 0;
|
|
591
|
+
const egress = row$1.capability === "net:fetch" ? host : shell;
|
|
592
|
+
if (egress !== void 0) {
|
|
593
|
+
const token = row$1.capability === "net:fetch" ? pathOf(target).find((s) => TOKEN_SEGMENT.has(s)) : void 0;
|
|
594
|
+
const where = row$1.capability === "net:fetch" ? `the destination ${hostOf(target) ?? target} is outside the egress list` : `the shell ran the network client ${row$1.command?.program}`;
|
|
595
|
+
return token !== void 0 ? {
|
|
596
|
+
stage: stage("token-abuse"),
|
|
597
|
+
basis: `${where} (${egress.id}), and the path names a token endpoint (/${token})`
|
|
598
|
+
} : {
|
|
599
|
+
stage: stage("exfiltration"),
|
|
600
|
+
basis: `${where} (${egress.id})`
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
if (row$1.screening !== void 0 && (row$1.screening.outcome === "steer" || row$1.screening.outcome === "hold")) return {
|
|
604
|
+
stage: stage("untrusted-input"),
|
|
605
|
+
basis: `screening found instructions in the content (${row$1.screening.outcome})`
|
|
606
|
+
};
|
|
607
|
+
if (row$1.capability === "fs:read" && target !== "" && withinAny(target, UNTRUSTED_GLOBS)) return {
|
|
608
|
+
stage: stage("untrusted-input"),
|
|
609
|
+
basis: `${target} is customer-supplied content`
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const EXECUTION_LABEL = {
|
|
613
|
+
"blocked-before-execution": "BLOCKED BEFORE EXECUTION",
|
|
614
|
+
"held-before-execution": "HELD BEFORE EXECUTION",
|
|
615
|
+
"executed-output-withheld": "EXECUTED · DETECTED AFTER EXECUTION",
|
|
616
|
+
"executed-shadow": "EXECUTED · SHADOW (WOULD HAVE BEEN REFUSED)",
|
|
617
|
+
executed: "EXECUTED",
|
|
618
|
+
permitted: "PERMITTED"
|
|
619
|
+
};
|
|
620
|
+
/**
|
|
621
|
+
* Whether the call ran, from the row alone. A refusal before execution and
|
|
622
|
+
* a result withheld after it are different claims, and the row says which:
|
|
623
|
+
* `phase: output` is the second.
|
|
624
|
+
*/
|
|
625
|
+
function executionOf(row$1) {
|
|
626
|
+
if (row$1.tool.startsWith("prae:")) return void 0;
|
|
627
|
+
const refused = row$1.verdict === "deny" || row$1.verdict === "correct";
|
|
628
|
+
if (row$1.shadow === true && (refused || row$1.verdict === "ask")) return "executed-shadow";
|
|
629
|
+
if (row$1.phase === "output") return refused ? "executed-output-withheld" : "executed";
|
|
630
|
+
if (refused) return "blocked-before-execution";
|
|
631
|
+
if (row$1.verdict === "ask") return "held-before-execution";
|
|
632
|
+
return row$1.executed === true ? "executed" : "permitted";
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* The Attack Chain Integrity Matrix: every classified row, in ledger order,
|
|
636
|
+
* from attempted action to the seal that proves it. The Guardian comes from
|
|
637
|
+
* the catalogue the caller passes; the rest is the row's own.
|
|
638
|
+
*/
|
|
639
|
+
function integrityMatrix(entries, pack, guardianOf = () => void 0) {
|
|
640
|
+
const out = [];
|
|
641
|
+
for (const row$1 of entries) {
|
|
642
|
+
const c = classify(row$1, pack);
|
|
643
|
+
const execution = executionOf(row$1);
|
|
644
|
+
if (c === void 0 || execution === void 0) continue;
|
|
645
|
+
const rule = row$1.rule ?? row$1.trace?.[0]?.rule;
|
|
646
|
+
const mismatch = entries.find((e) => e.rule === "skill.capability_mismatch" && e.about === row$1.seq)?.seq;
|
|
647
|
+
out.push({
|
|
648
|
+
seq: row$1.seq,
|
|
649
|
+
at: row$1.at,
|
|
650
|
+
action: row$1.tool,
|
|
651
|
+
...row$1.subject === void 0 ? {} : { target: row$1.subject },
|
|
652
|
+
...row$1.capability === void 0 ? {} : { capability: row$1.capability },
|
|
653
|
+
...guardianOf(rule) === void 0 ? {} : { guardian: guardianOf(rule) },
|
|
654
|
+
...rule === void 0 ? {} : {
|
|
655
|
+
rule,
|
|
656
|
+
ruleRole: row$1.rule === void 0 ? "recorded" : "decided"
|
|
657
|
+
},
|
|
658
|
+
decision: row$1.verdict.toUpperCase(),
|
|
659
|
+
...row$1.reason === void 0 ? {} : { reason: row$1.reason },
|
|
660
|
+
execution: EXECUTION_LABEL[execution],
|
|
661
|
+
stage: `${c.stage.step}/5 ${c.stage.name}`,
|
|
662
|
+
basis: c.basis,
|
|
663
|
+
...mismatch === void 0 ? {} : { mismatch },
|
|
664
|
+
hash: row$1.hash,
|
|
665
|
+
prev: row$1.prev,
|
|
666
|
+
signed: row$1.sig !== void 0
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region ../dsh/src/packset.ts
|
|
674
|
+
const PACK_FILE = /\.prae\.(json|ya?ml)$/;
|
|
675
|
+
/** Read one pack file: its parsed pack, or why it cannot be one. */
|
|
676
|
+
function readPackFile(file, policyKey, origin = "workspace") {
|
|
677
|
+
let bytes;
|
|
678
|
+
try {
|
|
679
|
+
bytes = readFileSync(file);
|
|
680
|
+
} catch (e) {
|
|
681
|
+
return {
|
|
682
|
+
file,
|
|
683
|
+
reason: `unreadable: ${e instanceof Error ? e.message : String(e)}`
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
687
|
+
if (policyKey !== void 0) {
|
|
688
|
+
let signature = "";
|
|
689
|
+
try {
|
|
690
|
+
signature = readFileSync(`${file}.sig`, "utf8");
|
|
691
|
+
} catch {}
|
|
692
|
+
if (signature === "") return {
|
|
693
|
+
file,
|
|
694
|
+
reason: `unsigned: no ${file.split("/").at(-1)}.sig beside it, and a policy key is pinned`
|
|
695
|
+
};
|
|
696
|
+
if (!verifyBytes(bytes, signature, policyKey)) return {
|
|
697
|
+
file,
|
|
698
|
+
reason: `not signed by the policy key ${fingerprint(policyKey)} (edited after signing, or signed by another key)`
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
let json = bytes.toString("utf8");
|
|
702
|
+
if (/\.ya?ml$/.test(file)) try {
|
|
703
|
+
json = JSON.stringify(parse(json));
|
|
704
|
+
} catch (e) {
|
|
705
|
+
return {
|
|
706
|
+
file,
|
|
707
|
+
reason: `invalid YAML: ${e instanceof Error ? e.message : String(e)}`
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const parsed = parsePack(json);
|
|
711
|
+
if (!parsed.ok) return {
|
|
712
|
+
file,
|
|
713
|
+
reason: parsed.errors.map((e) => `${e.path || "<root>"}: ${e.message}`).join("; ")
|
|
714
|
+
};
|
|
715
|
+
return {
|
|
716
|
+
file,
|
|
717
|
+
pack: parsed.pack,
|
|
718
|
+
origin,
|
|
719
|
+
sha256,
|
|
720
|
+
...policyKey === void 0 ? {} : { signedBy: fingerprint(policyKey) }
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/** Load, validate and merge every pack in a directory. Never throws. */
|
|
724
|
+
function loadPackDir(dir, policyKey, accept, builtin) {
|
|
725
|
+
const packs = [];
|
|
726
|
+
const rejected = [];
|
|
727
|
+
const ids = /* @__PURE__ */ new Map();
|
|
728
|
+
let baselineFrom;
|
|
729
|
+
let names = [];
|
|
730
|
+
try {
|
|
731
|
+
names = readdirSync(resolve(dir)).filter((n) => PACK_FILE.test(n)).sort();
|
|
732
|
+
} catch (e) {
|
|
733
|
+
rejected.push({
|
|
734
|
+
file: dir,
|
|
735
|
+
reason: `no such directory: ${e instanceof Error ? e.message : String(e)}`
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const candidates = [...builtin === void 0 ? [] : [builtin], ...names.map((name) => readPackFile(join(resolve(dir), name), policyKey))];
|
|
739
|
+
for (const read of candidates) {
|
|
740
|
+
const file = read.file;
|
|
741
|
+
const name = file.split("/").at(-1) ?? file;
|
|
742
|
+
if (!("pack" in read)) {
|
|
743
|
+
rejected.push(read);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const refused = accept?.(read);
|
|
747
|
+
if (refused !== void 0) {
|
|
748
|
+
rejected.push({
|
|
749
|
+
file,
|
|
750
|
+
reason: refused
|
|
751
|
+
});
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const foreign = read.origin === "workspace" ? read.pack.rules.find((r) => !r.id.startsWith("ORG-")) : void 0;
|
|
755
|
+
if (foreign !== void 0) {
|
|
756
|
+
rejected.push({
|
|
757
|
+
file,
|
|
758
|
+
reason: `rule id ${foreign.id} must start with ORG-; workspace rules are the organisation's own, and other ids belong to built-in packs`
|
|
759
|
+
});
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const clash = read.pack.rules.find((r) => ids.has(r.id));
|
|
763
|
+
if (clash !== void 0) {
|
|
764
|
+
rejected.push({
|
|
765
|
+
file,
|
|
766
|
+
reason: `rule id ${clash.id} is already defined by ${ids.get(clash.id)}; rule ids must be unique across the workspace`
|
|
767
|
+
});
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (read.pack.baseline !== void 0 && baselineFrom !== void 0) {
|
|
771
|
+
rejected.push({
|
|
772
|
+
file,
|
|
773
|
+
reason: `declares a baseline, and ${baselineFrom} already does; only one pack in a workspace may, so a grant is never widened`
|
|
774
|
+
});
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (read.pack.baseline !== void 0) baselineFrom = name;
|
|
778
|
+
for (const r of read.pack.rules) ids.set(r.id, name);
|
|
779
|
+
packs.push(read);
|
|
780
|
+
}
|
|
781
|
+
return merge(packs, rejected);
|
|
782
|
+
}
|
|
783
|
+
/** The packs as one policy, each rule remembering where it came from. */
|
|
784
|
+
function merge(packs, rejected) {
|
|
785
|
+
const origins = /* @__PURE__ */ new Map();
|
|
786
|
+
for (const p of packs) for (const r of p.pack.rules) origins.set(r.id, {
|
|
787
|
+
name: p.pack.pack,
|
|
788
|
+
version: p.pack.version,
|
|
789
|
+
sha256: p.sha256,
|
|
790
|
+
origin: p.origin
|
|
791
|
+
});
|
|
792
|
+
if (packs.length === 0) return {
|
|
793
|
+
packs,
|
|
794
|
+
rejected,
|
|
795
|
+
origins,
|
|
796
|
+
effective: {
|
|
797
|
+
prae: "0.1",
|
|
798
|
+
pack: "no-valid-pack",
|
|
799
|
+
version: "0",
|
|
800
|
+
rules: [{
|
|
801
|
+
id: "PRAE-NO-PACK",
|
|
802
|
+
when: {},
|
|
803
|
+
effect: {
|
|
804
|
+
kind: "deny",
|
|
805
|
+
because: `no valid policy pack is loaded, so nothing runs (${rejected.length === 0 ? "no policy pack found" : rejected.map((r) => `${r.file.split("/").at(-1)}: ${r.reason}`).join(" · ")})`
|
|
806
|
+
}
|
|
807
|
+
}]
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
const baseline = packs.find((p) => p.pack.baseline !== void 0)?.pack.baseline;
|
|
811
|
+
const only = packs.length === 1 ? packs[0] : void 0;
|
|
812
|
+
return {
|
|
813
|
+
packs,
|
|
814
|
+
rejected,
|
|
815
|
+
origins,
|
|
816
|
+
effective: {
|
|
817
|
+
prae: "0.1",
|
|
818
|
+
pack: only?.pack.pack ?? packs.map((p) => p.pack.pack).join("+"),
|
|
819
|
+
version: only?.pack.version ?? createHash("sha256").update(packs.map((p) => p.sha256).join("")).digest("hex").slice(0, 12),
|
|
820
|
+
...baseline === void 0 ? {} : { baseline },
|
|
821
|
+
rules: packs.flatMap((p) => p.pack.rules)
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
440
826
|
//#endregion
|
|
441
827
|
//#region src/console/ledger.ts
|
|
442
828
|
function ago(iso, now) {
|
|
@@ -763,6 +1149,7 @@ function praeRow(options) {
|
|
|
763
1149
|
if (options.ledger !== void 0) lines.push(` ledger: ${JSON.stringify(options.ledger)}`);
|
|
764
1150
|
if (options.approver !== void 0) lines.push(` approver: ${JSON.stringify(options.approver)}`);
|
|
765
1151
|
if (options.mode !== void 0) lines.push(` mode: ${options.mode}`);
|
|
1152
|
+
if (options.packDir !== void 0) lines.push(` packDir: ${JSON.stringify(options.packDir)}`);
|
|
766
1153
|
if (options.packFile !== void 0) lines.push(` packFile: ${JSON.stringify(options.packFile)}`);
|
|
767
1154
|
else lines.push(" pack: |", indent(options.pack ?? "", 10));
|
|
768
1155
|
return lines.join("\n");
|
|
@@ -832,6 +1219,7 @@ const DEFAULT_FILES = {
|
|
|
832
1219
|
report: "security-report.json",
|
|
833
1220
|
guardians: "guardians.json",
|
|
834
1221
|
pack: "papaya.prae.json",
|
|
1222
|
+
packs: ".prae/packs",
|
|
835
1223
|
publicKey: "prae-signing.pub.pem",
|
|
836
1224
|
redteam: "../redteam/results"
|
|
837
1225
|
};
|
|
@@ -849,12 +1237,358 @@ function readRedteam(dir, files) {
|
|
|
849
1237
|
const root = resolve(dir, files.redteam);
|
|
850
1238
|
if (!existsSync(root)) return {
|
|
851
1239
|
campaign: null,
|
|
852
|
-
runs: []
|
|
1240
|
+
runs: [],
|
|
1241
|
+
steering: []
|
|
853
1242
|
};
|
|
854
1243
|
const runs = readdirSync(root).filter((n) => n.endsWith(".json")).sort().filter((n) => n !== "campaign.json").map((n) => jsonOrNull(resolve(root, n))).filter((r) => r !== null);
|
|
1244
|
+
const steeringDir = resolve(root, "steering");
|
|
1245
|
+
const steering = existsSync(steeringDir) ? readdirSync(steeringDir).filter((n) => n.endsWith(".json")).sort().map((n) => jsonOrNull(resolve(steeringDir, n))).filter((r) => r !== null && typeof r === "object" && "objective" in r) : [];
|
|
855
1246
|
return {
|
|
856
1247
|
campaign: jsonOrNull(resolve(root, "campaign.json")),
|
|
857
|
-
runs
|
|
1248
|
+
runs,
|
|
1249
|
+
steering
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
//#endregion
|
|
1254
|
+
//#region ../controls/src/scope.ts
|
|
1255
|
+
const ROW_LIMIT = 50;
|
|
1256
|
+
/** A framework's edition, read from its versioned id (`…-2026`). */
|
|
1257
|
+
const versionOf = (id) => /(\d{4})$/.exec(id)?.[1];
|
|
1258
|
+
/** Where a built-in control belongs, and its code within it. */
|
|
1259
|
+
function placeOf(control) {
|
|
1260
|
+
const found = Object.entries(FRAMEWORKS).find(([, f]) => control.id.startsWith(`${f.short} · `));
|
|
1261
|
+
if (found !== void 0) return {
|
|
1262
|
+
framework: {
|
|
1263
|
+
id: found[0],
|
|
1264
|
+
name: found[1].name
|
|
1265
|
+
},
|
|
1266
|
+
id: control.id.slice(found[1].short.length + 3)
|
|
1267
|
+
};
|
|
1268
|
+
return {
|
|
1269
|
+
framework: control.standard ?? {
|
|
1270
|
+
id: "obligations",
|
|
1271
|
+
name: "Obligations"
|
|
1272
|
+
},
|
|
1273
|
+
id: control.code ?? control.id.split(" · ")[1] ?? control.id
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
/** A control defined as data: evidence is the rows under its mapped rules. */
|
|
1277
|
+
function fromData(def) {
|
|
1278
|
+
const types = new Set(def.evidence_types ?? [
|
|
1279
|
+
"allow",
|
|
1280
|
+
"deny",
|
|
1281
|
+
"ask"
|
|
1282
|
+
]);
|
|
1283
|
+
const trace = (data) => {
|
|
1284
|
+
const ids = new Set(def.prae_controls);
|
|
1285
|
+
return {
|
|
1286
|
+
rules: [...def.prae_controls],
|
|
1287
|
+
rows: data.entries.filter((e) => e.rule !== void 0 && ids.has(e.rule) && types.has(e.verdict))
|
|
1288
|
+
};
|
|
1289
|
+
};
|
|
1290
|
+
return {
|
|
1291
|
+
id: def.id,
|
|
1292
|
+
...def.name === void 0 ? {} : { name: def.name },
|
|
1293
|
+
asks: def.asks,
|
|
1294
|
+
trace,
|
|
1295
|
+
evidence: (data) => {
|
|
1296
|
+
const inPack = new Set((data.pack?.rules ?? []).map((r) => r.id));
|
|
1297
|
+
const present = def.prae_controls.filter((id) => inPack.has(id));
|
|
1298
|
+
const rows = trace(data).rows.length;
|
|
1299
|
+
if (rows > 0) return {
|
|
1300
|
+
state: "covered",
|
|
1301
|
+
note: `${def.prae_controls.length} PRAE control${def.prae_controls.length === 1 ? "" : "s"} mapped (${def.prae_controls.join(", ")}) · ${rows} sealed decision${rows === 1 ? "" : "s"} under them in scope.`
|
|
1302
|
+
};
|
|
1303
|
+
if (present.length > 0) return {
|
|
1304
|
+
state: "partial",
|
|
1305
|
+
note: `${present.length} mapped PRAE control${present.length === 1 ? " is" : "s are"} in the pack (${present.join(", ")}); none has decided a call in scope, so the mapping is declared, not exercised.`
|
|
1306
|
+
};
|
|
1307
|
+
return {
|
|
1308
|
+
state: "absent",
|
|
1309
|
+
note: def.prae_controls.length === 0 ? "No PRAE control is mapped to it." : `The mapped PRAE controls (${def.prae_controls.join(", ")}) are not in this pack.`
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
/** The built-in catalog, plus a record's own frameworks when it has any. */
|
|
1315
|
+
function catalogOf(file) {
|
|
1316
|
+
const frameworks = /* @__PURE__ */ new Map();
|
|
1317
|
+
const controls = /* @__PURE__ */ new Map();
|
|
1318
|
+
const add = (framework, id, control) => {
|
|
1319
|
+
const key = `${framework.id}:${id}`;
|
|
1320
|
+
if (controls.has(key)) return;
|
|
1321
|
+
controls.set(key, control);
|
|
1322
|
+
let f = frameworks.get(framework.id);
|
|
1323
|
+
if (f === void 0) {
|
|
1324
|
+
f = {
|
|
1325
|
+
...framework,
|
|
1326
|
+
controls: []
|
|
1327
|
+
};
|
|
1328
|
+
frameworks.set(framework.id, f);
|
|
1329
|
+
}
|
|
1330
|
+
f.controls.push({
|
|
1331
|
+
key,
|
|
1332
|
+
id,
|
|
1333
|
+
...control.name === void 0 ? {} : { name: control.name },
|
|
1334
|
+
asks: control.asks
|
|
1335
|
+
});
|
|
1336
|
+
};
|
|
1337
|
+
for (const control of CONTROLS) {
|
|
1338
|
+
const place = placeOf(control);
|
|
1339
|
+
const version = versionOf(place.framework.id);
|
|
1340
|
+
add({
|
|
1341
|
+
id: place.framework.id,
|
|
1342
|
+
name: place.framework.name,
|
|
1343
|
+
...version === void 0 ? {} : { version }
|
|
1344
|
+
}, place.id, control);
|
|
1345
|
+
}
|
|
1346
|
+
for (const f of file?.frameworks ?? []) for (const c of f.controls) add({
|
|
1347
|
+
id: f.id,
|
|
1348
|
+
name: f.name,
|
|
1349
|
+
...f.version === void 0 ? {} : { version: f.version },
|
|
1350
|
+
...f.description === void 0 ? {} : { description: f.description },
|
|
1351
|
+
custom: true
|
|
1352
|
+
}, c.id, fromData(c));
|
|
1353
|
+
return {
|
|
1354
|
+
frameworks: [...frameworks.values()],
|
|
1355
|
+
controls
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
/** A catalog file, checked: what is wrong with it, or the file. */
|
|
1359
|
+
function parseCatalog(raw) {
|
|
1360
|
+
const text = (v) => typeof v === "string" && v.trim() !== "";
|
|
1361
|
+
if (typeof raw !== "object" || raw === null) return "not an object";
|
|
1362
|
+
const frameworks = raw.frameworks;
|
|
1363
|
+
if (!Array.isArray(frameworks)) return "frameworks: expected an array";
|
|
1364
|
+
for (const [i, f] of frameworks.entries()) {
|
|
1365
|
+
if (!text(f?.id) || !/^[a-z0-9][a-z0-9-]*$/.test(f.id)) return `frameworks[${i}].id: lowercase letters, digits and dashes`;
|
|
1366
|
+
if (!text(f.name)) return `frameworks[${i}].name: required`;
|
|
1367
|
+
if (!Array.isArray(f.controls)) return `frameworks[${i}].controls: expected an array`;
|
|
1368
|
+
for (const [j, c] of f.controls.entries()) {
|
|
1369
|
+
const at = `frameworks[${i}].controls[${j}]`;
|
|
1370
|
+
if (!text(c?.id)) return `${at}.id: required`;
|
|
1371
|
+
if (!text(c.asks)) return `${at}.asks: required`;
|
|
1372
|
+
if (!Array.isArray(c.prae_controls) || !c.prae_controls.every((r) => text(r))) return `${at}.prae_controls: expected an array of PRAE rule ids`;
|
|
1373
|
+
if (c.evidence_types !== void 0 && (!Array.isArray(c.evidence_types) || !c.evidence_types.every((t) => [
|
|
1374
|
+
"allow",
|
|
1375
|
+
"deny",
|
|
1376
|
+
"ask"
|
|
1377
|
+
].includes(t)))) return `${at}.evidence_types: allow, deny or ask`;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return raw;
|
|
1381
|
+
}
|
|
1382
|
+
/** Everything built in, every agent and session, the whole record. */
|
|
1383
|
+
function defaultScope(catalog) {
|
|
1384
|
+
const builtIn = catalog.frameworks.filter((f) => f.custom !== true);
|
|
1385
|
+
return {
|
|
1386
|
+
frameworks: builtIn.map((f) => f.id),
|
|
1387
|
+
controls: builtIn.flatMap((f) => f.controls.map((c) => c.key)),
|
|
1388
|
+
agents: "all",
|
|
1389
|
+
sessions: "all",
|
|
1390
|
+
period: { kind: "all" }
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
/** A scope as saved or sent, checked: what is wrong with it, or the scope. */
|
|
1394
|
+
function parseScope(raw) {
|
|
1395
|
+
if (typeof raw !== "object" || raw === null) return "not an object";
|
|
1396
|
+
const r = raw;
|
|
1397
|
+
const strings = (v) => Array.isArray(v) && v.every((s) => typeof s === "string" && s.length <= 200);
|
|
1398
|
+
if (!strings(r.frameworks)) return "frameworks: expected an array of ids";
|
|
1399
|
+
if (!strings(r.controls)) return "controls: expected an array of keys";
|
|
1400
|
+
for (const d of ["agents", "sessions"]) if (r[d] !== "all" && !strings(r[d])) return `${d}: "all" or an array`;
|
|
1401
|
+
const p = r.period;
|
|
1402
|
+
const period = p?.kind === "all" ? { kind: "all" } : p?.kind === "days" && typeof p.days === "number" && Number.isInteger(p.days) && p.days > 0 && p.days <= 3650 ? {
|
|
1403
|
+
kind: "days",
|
|
1404
|
+
days: p.days
|
|
1405
|
+
} : p?.kind === "custom" && typeof p.from === "string" && typeof p.to === "string" && !Number.isNaN(Date.parse(p.from)) && !Number.isNaN(Date.parse(p.to)) && Date.parse(p.from) <= Date.parse(p.to) ? {
|
|
1406
|
+
kind: "custom",
|
|
1407
|
+
from: p.from,
|
|
1408
|
+
to: p.to
|
|
1409
|
+
} : "period: all, the last N days, or a custom from–to range";
|
|
1410
|
+
if (typeof period === "string") return period;
|
|
1411
|
+
return {
|
|
1412
|
+
frameworks: r.frameworks,
|
|
1413
|
+
controls: r.controls,
|
|
1414
|
+
agents: r.agents,
|
|
1415
|
+
sessions: r.sessions,
|
|
1416
|
+
period
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
/** Whether a row falls in the scope's period. */
|
|
1420
|
+
function inPeriod(at, period, now) {
|
|
1421
|
+
const t = Date.parse(at);
|
|
1422
|
+
if (period.kind === "all") return true;
|
|
1423
|
+
if (Number.isNaN(t)) return false;
|
|
1424
|
+
if (period.kind === "days") return t >= now - period.days * 864e5;
|
|
1425
|
+
return t >= Date.parse(period.from) && t < Date.parse(period.to) + 864e5;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Assess the scoped controls against the record. The rows are narrowed to
|
|
1429
|
+
* the scope's agents, sessions and period before any control reads them;
|
|
1430
|
+
* the chain's verification stays the whole record's, since integrity is a
|
|
1431
|
+
* property of the chain, not of a slice of it.
|
|
1432
|
+
*/
|
|
1433
|
+
function assess(data, scope, catalog, options) {
|
|
1434
|
+
const now = options.now ?? Date.now();
|
|
1435
|
+
const agentOf$1 = (e) => e.agent ?? options.agent;
|
|
1436
|
+
const admitted = data.entries.filter((e) => (scope.agents === "all" || scope.agents.includes(agentOf$1(e))) && (scope.sessions === "all" || e.session !== void 0 && scope.sessions.includes(e.session)) && inPeriod(e.at, scope.period, now));
|
|
1437
|
+
const scoped = {
|
|
1438
|
+
...data,
|
|
1439
|
+
entries: admitted
|
|
1440
|
+
};
|
|
1441
|
+
const frameworksOn = new Set(scope.frameworks);
|
|
1442
|
+
const controlsOn = new Set(scope.controls);
|
|
1443
|
+
const row$1 = (e) => ({
|
|
1444
|
+
seq: e.seq,
|
|
1445
|
+
at: e.at,
|
|
1446
|
+
tool: e.tool,
|
|
1447
|
+
...e.subject === void 0 ? {} : { subject: e.subject },
|
|
1448
|
+
verdict: e.verdict,
|
|
1449
|
+
...e.rule === void 0 ? {} : { rule: e.rule },
|
|
1450
|
+
...e.session === void 0 ? {} : { session: e.session },
|
|
1451
|
+
agent: agentOf$1(e),
|
|
1452
|
+
hash: e.hash
|
|
1453
|
+
});
|
|
1454
|
+
const frameworks = catalog.frameworks.map((f) => {
|
|
1455
|
+
const selected = frameworksOn.has(f.id);
|
|
1456
|
+
const controls = f.controls.map((c) => {
|
|
1457
|
+
const control = catalog.controls.get(c.key);
|
|
1458
|
+
if (!(selected && controlsOn.has(c.key)) || control === void 0) return {
|
|
1459
|
+
...c,
|
|
1460
|
+
inScope: false,
|
|
1461
|
+
state: "not-in-scope",
|
|
1462
|
+
note: "Not in the selected scope, so not assessed.",
|
|
1463
|
+
rules: [],
|
|
1464
|
+
evidence: {
|
|
1465
|
+
count: 0,
|
|
1466
|
+
rows: []
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
const found = control.evidence(scoped);
|
|
1470
|
+
const trace = control.trace?.(scoped) ?? {
|
|
1471
|
+
rules: [],
|
|
1472
|
+
rows: []
|
|
1473
|
+
};
|
|
1474
|
+
return {
|
|
1475
|
+
...c,
|
|
1476
|
+
inScope: true,
|
|
1477
|
+
state: found.state,
|
|
1478
|
+
note: found.note,
|
|
1479
|
+
rules: trace.rules,
|
|
1480
|
+
evidence: {
|
|
1481
|
+
count: trace.rows.length,
|
|
1482
|
+
rows: trace.rows.slice(-ROW_LIMIT).reverse().map(row$1)
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
});
|
|
1486
|
+
const on$1 = controls.filter((c) => c.inScope);
|
|
1487
|
+
const count = (s) => on$1.filter((c) => c.state === s).length;
|
|
1488
|
+
const { controls: _,...meta } = f;
|
|
1489
|
+
return {
|
|
1490
|
+
...meta,
|
|
1491
|
+
selected,
|
|
1492
|
+
total: controls.length,
|
|
1493
|
+
inScope: on$1.length,
|
|
1494
|
+
covered: count("covered"),
|
|
1495
|
+
partial: count("partial"),
|
|
1496
|
+
absent: count("absent"),
|
|
1497
|
+
controls
|
|
1498
|
+
};
|
|
1499
|
+
});
|
|
1500
|
+
const on = frameworks.flatMap((f) => f.controls.filter((c) => c.inScope));
|
|
1501
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1502
|
+
for (const e of data.entries) {
|
|
1503
|
+
if (e.session === void 0) continue;
|
|
1504
|
+
const s = sessions.get(e.session);
|
|
1505
|
+
if (s === void 0) sessions.set(e.session, {
|
|
1506
|
+
rows: 1,
|
|
1507
|
+
first: e.at,
|
|
1508
|
+
last: e.at
|
|
1509
|
+
});
|
|
1510
|
+
else {
|
|
1511
|
+
s.rows += 1;
|
|
1512
|
+
s.last = e.at;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return {
|
|
1516
|
+
scope,
|
|
1517
|
+
frameworks,
|
|
1518
|
+
totals: {
|
|
1519
|
+
frameworks: frameworks.filter((f) => f.inScope > 0).length,
|
|
1520
|
+
controls: on.length,
|
|
1521
|
+
withEvidence: on.filter((c) => c.state !== "absent").length,
|
|
1522
|
+
covered: on.filter((c) => c.state === "covered").length,
|
|
1523
|
+
partial: on.filter((c) => c.state === "partial").length,
|
|
1524
|
+
absent: on.filter((c) => c.state === "absent").length
|
|
1525
|
+
},
|
|
1526
|
+
dimensions: {
|
|
1527
|
+
agents: [...new Set([options.agent, ...data.entries.map(agentOf$1)])],
|
|
1528
|
+
sessions: [...sessions.entries()].map(([id, s]) => ({
|
|
1529
|
+
id,
|
|
1530
|
+
...s
|
|
1531
|
+
})),
|
|
1532
|
+
environment: options.environment
|
|
1533
|
+
},
|
|
1534
|
+
rows: {
|
|
1535
|
+
admitted: admitted.length,
|
|
1536
|
+
total: data.entries.length
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
//#endregion
|
|
1542
|
+
//#region src/compliance.ts
|
|
1543
|
+
const SCOPE_FILE = ".prae-compliance/scope.json";
|
|
1544
|
+
const CATALOG_FILE = "compliance-catalog.json";
|
|
1545
|
+
/** The record's catalog: built in, plus its own file when it parses. */
|
|
1546
|
+
function catalogFor(dir) {
|
|
1547
|
+
const path = resolve(dir, CATALOG_FILE);
|
|
1548
|
+
if (!existsSync(path)) return { catalog: catalogOf() };
|
|
1549
|
+
let raw;
|
|
1550
|
+
try {
|
|
1551
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
1552
|
+
} catch {
|
|
1553
|
+
return {
|
|
1554
|
+
catalog: catalogOf(),
|
|
1555
|
+
catalogError: `${CATALOG_FILE}: not JSON`
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
const file = parseCatalog(raw);
|
|
1559
|
+
return typeof file === "string" ? {
|
|
1560
|
+
catalog: catalogOf(),
|
|
1561
|
+
catalogError: `${CATALOG_FILE}: ${file}`
|
|
1562
|
+
} : { catalog: catalogOf(file) };
|
|
1563
|
+
}
|
|
1564
|
+
/** The saved scope, or everything built in when none is saved yet. */
|
|
1565
|
+
function scopeFor(dir, catalog) {
|
|
1566
|
+
const path = resolve(dir, SCOPE_FILE);
|
|
1567
|
+
if (!existsSync(path)) return defaultScope(catalog);
|
|
1568
|
+
try {
|
|
1569
|
+
const scope = parseScope(JSON.parse(readFileSync(path, "utf8")));
|
|
1570
|
+
return typeof scope === "string" ? defaultScope(catalog) : scope;
|
|
1571
|
+
} catch {
|
|
1572
|
+
return defaultScope(catalog);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/** The agent the record's discovery names, for rows that name none. */
|
|
1576
|
+
function agentOf(dir) {
|
|
1577
|
+
try {
|
|
1578
|
+
const discovery = JSON.parse(readFileSync(resolve(dir, "discovery.json"), "utf8"));
|
|
1579
|
+
if (typeof discovery.agent?.name === "string") return discovery.agent.name;
|
|
1580
|
+
} catch {}
|
|
1581
|
+
return "this agent";
|
|
1582
|
+
}
|
|
1583
|
+
function assessCompliance(record, data) {
|
|
1584
|
+
const { catalog, catalogError } = catalogFor(record.dir);
|
|
1585
|
+
return {
|
|
1586
|
+
...assess(data, scopeFor(record.dir, catalog), catalog, {
|
|
1587
|
+
agent: agentOf(record.dir),
|
|
1588
|
+
environment: basename(resolve(record.dir))
|
|
1589
|
+
}),
|
|
1590
|
+
saved: existsSync(resolve(record.dir, SCOPE_FILE)),
|
|
1591
|
+
...catalogError === void 0 ? {} : { catalogError }
|
|
858
1592
|
};
|
|
859
1593
|
}
|
|
860
1594
|
|
|
@@ -972,6 +1706,45 @@ function verifyRecord(record, tamper, repair = false) {
|
|
|
972
1706
|
signatures
|
|
973
1707
|
};
|
|
974
1708
|
}
|
|
1709
|
+
/** The record's built-in pack and workspace packs, as the gate loads them. */
|
|
1710
|
+
function packSetOf(record) {
|
|
1711
|
+
const { dir, files } = record;
|
|
1712
|
+
const packsDir = resolve(dir, files.packs);
|
|
1713
|
+
const builtinPath = resolve(dir, files.pack);
|
|
1714
|
+
return {
|
|
1715
|
+
packsDir,
|
|
1716
|
+
set: loadPackDir(packsDir, void 0, void 0, existsSync(builtinPath) ? readPackFile(builtinPath, void 0, "builtin") : void 0)
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* The packs in force for a record (PRA-127): the built-in pack and the
|
|
1721
|
+
* workspace's own, read by the gate's loader — each with its origin, version,
|
|
1722
|
+
* sha256 and rules — and every workspace pack the loader rejects, with why.
|
|
1723
|
+
*/
|
|
1724
|
+
function packsOf(record) {
|
|
1725
|
+
const { packsDir, set } = packSetOf(record);
|
|
1726
|
+
return {
|
|
1727
|
+
packs: set.packs.map((p) => ({
|
|
1728
|
+
file: p.file.split("/").at(-1),
|
|
1729
|
+
name: p.pack.pack,
|
|
1730
|
+
version: p.pack.version,
|
|
1731
|
+
...p.pack.description === void 0 ? {} : { description: p.pack.description },
|
|
1732
|
+
...p.pack.owner === void 0 ? {} : { owner: p.pack.owner },
|
|
1733
|
+
sha256: p.sha256,
|
|
1734
|
+
origin: p.origin,
|
|
1735
|
+
rules: p.pack.rules.map((r) => ({
|
|
1736
|
+
id: r.id,
|
|
1737
|
+
...r.title === void 0 ? {} : { title: r.title },
|
|
1738
|
+
kind: r.effect.kind,
|
|
1739
|
+
because: r.effect.because
|
|
1740
|
+
}))
|
|
1741
|
+
})),
|
|
1742
|
+
rejected: set.rejected.filter((r) => r.file !== packsDir).map((r) => ({
|
|
1743
|
+
file: r.file.split("/").at(-1),
|
|
1744
|
+
reason: r.reason
|
|
1745
|
+
}))
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
975
1748
|
/**
|
|
976
1749
|
* Evidence toward each control, from the pack and the ledger as they are
|
|
977
1750
|
* now — the rows the controls package defines, so the console, the exporter and
|
|
@@ -988,31 +1761,32 @@ function complianceOf(record) {
|
|
|
988
1761
|
} catch {
|
|
989
1762
|
entries = [];
|
|
990
1763
|
}
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
if (existsSync(packPath)) {
|
|
994
|
-
const parsed = parsePack(readFileSync(packPath, "utf8"));
|
|
995
|
-
if (parsed.ok) pack = parsed.pack;
|
|
996
|
-
}
|
|
1764
|
+
const { set } = packSetOf(record);
|
|
1765
|
+
const pack = set.packs.length > 0 ? set.effective : void 0;
|
|
997
1766
|
const data = {
|
|
998
1767
|
...pack === void 0 ? {} : { pack },
|
|
999
1768
|
entries,
|
|
1000
1769
|
verification: verifyEntries(text),
|
|
1001
1770
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1002
1771
|
};
|
|
1003
|
-
return {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1772
|
+
return {
|
|
1773
|
+
assessment: assessCompliance(record, data),
|
|
1774
|
+
controls: CONTROLS.map((control) => {
|
|
1775
|
+
const found = Object.entries(FRAMEWORKS).find(([, f]) => control.id.startsWith(`${f.short} · `));
|
|
1776
|
+
const code = found === void 0 ? void 0 : control.id.split(" · ")[1];
|
|
1777
|
+
const named = found === void 0 || code === void 0 ? void 0 : found[1].controls[code];
|
|
1778
|
+
const standard = control.standard;
|
|
1779
|
+
const name = named?.name ?? control.name;
|
|
1780
|
+
return {
|
|
1781
|
+
id: code ?? (standard === void 0 ? control.id : control.id.split(" · ")[1] ?? control.id),
|
|
1782
|
+
...name === void 0 ? {} : { name },
|
|
1783
|
+
asks: control.asks,
|
|
1784
|
+
framework: found?.[0] ?? standard?.id ?? "obligation",
|
|
1785
|
+
...found === void 0 ? standard === void 0 ? {} : { frameworkName: standard.name } : { frameworkName: found[1].name },
|
|
1786
|
+
...control.evidence(data)
|
|
1787
|
+
};
|
|
1788
|
+
})
|
|
1789
|
+
};
|
|
1016
1790
|
}
|
|
1017
1791
|
/**
|
|
1018
1792
|
* The record over HTTP, for the Command Center (apps/console): the same
|
|
@@ -1069,6 +1843,10 @@ function handleApi(req, res, record) {
|
|
|
1069
1843
|
json(complianceOf(record));
|
|
1070
1844
|
return true;
|
|
1071
1845
|
}
|
|
1846
|
+
if (route === "/api/packs") {
|
|
1847
|
+
json(packsOf(record));
|
|
1848
|
+
return true;
|
|
1849
|
+
}
|
|
1072
1850
|
if (route === "/api/redteam") {
|
|
1073
1851
|
json(readRedteam(dir, files));
|
|
1074
1852
|
return true;
|
|
@@ -1167,6 +1945,12 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1167
1945
|
prae check <pack.json>
|
|
1168
1946
|
Validate a policy pack and summarise what it enforces.
|
|
1169
1947
|
|
|
1948
|
+
prae check <dir> [--policy-key <policy.pub.pem>]
|
|
1949
|
+
Validate a workspace's packs (e.g. .prae/packs) exactly as the gate
|
|
1950
|
+
loads them: every *.prae.json / .yaml in name order, each rejection
|
|
1951
|
+
with its reason, and, with --policy-key, each .sig checked. Exits
|
|
1952
|
+
non-zero if any pack would be rejected.
|
|
1953
|
+
|
|
1170
1954
|
prae explain <pack.json> <tool> [key=value ...]
|
|
1171
1955
|
Judge one call against the pack and print the trace. Nothing is executed.
|
|
1172
1956
|
|
|
@@ -1193,6 +1977,14 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1193
1977
|
--checkpoint, also check that nothing before that checkpoint's head
|
|
1194
1978
|
has been removed or rewritten since it was taken.
|
|
1195
1979
|
|
|
1980
|
+
prae chain <ledger.jsonl> --pack <pack.json> [--guardians <guardians.json>]
|
|
1981
|
+
[--key <public.pem>] [--json]
|
|
1982
|
+
The Attack Chain Integrity Matrix: each attack-chain row from the
|
|
1983
|
+
attempted action to its seal — capability, Guardian, rule, decision,
|
|
1984
|
+
whether it ran, the stage and why, the hash and the signature. The
|
|
1985
|
+
stage is classified from the sealed row and the pack, never typed in.
|
|
1986
|
+
Exits non-zero if the chain or (with --key) any signature fails.
|
|
1987
|
+
|
|
1196
1988
|
prae checkpoint <ledger.jsonl> [--key <private.pem>] [--out <file>]
|
|
1197
1989
|
Record the ledger's length and head, signed, to keep somewhere the
|
|
1198
1990
|
ledger's machine cannot write. A chain proves no row inside the file
|
|
@@ -1209,6 +2001,20 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1209
2001
|
Generate an Ed25519 keypair for signing ledger rows. Prints the
|
|
1210
2002
|
fingerprint a counterparty pins.
|
|
1211
2003
|
|
|
2004
|
+
prae keygen --role operator|policy|ledger [--out <dir>]
|
|
2005
|
+
A keypair whose private half is sealed under a passphrase (asked for
|
|
2006
|
+
without echo, or PRAE_KEY_PASSPHRASE). The operator key signs answers
|
|
2007
|
+
to held calls; the policy key signs packs. Pin the public half in the
|
|
2008
|
+
gate (operator.publicKey / policyKey); the agent can read the sealed
|
|
2009
|
+
file and still cannot sign. The ledger key is for the signer that
|
|
2010
|
+
holds it on the harness's behalf (signerFd): prae-signing.*.
|
|
2011
|
+
|
|
2012
|
+
prae sign-pack <pack.json> --key <policy.key.sealed.pem>
|
|
2013
|
+
Sign a pack's exact bytes with the sealed policy key (passphrase asked
|
|
2014
|
+
without echo, or PRAE_KEY_PASSPHRASE), writing <pack.json>.sig. A gate
|
|
2015
|
+
with policyKey pinned governs only by a signed pack whose version
|
|
2016
|
+
rises; raise the version before signing.
|
|
2017
|
+
|
|
1212
2018
|
prae receipt <ledger.jsonl> <seq> --key <private.pem> [--out <file>]
|
|
1213
2019
|
Export one decision as a self-contained signed receipt.
|
|
1214
2020
|
|
|
@@ -1236,13 +2042,16 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1236
2042
|
Read the instruction files at <dir> and propose the rules they imply.
|
|
1237
2043
|
Proposals only: an existing pack is never modified.
|
|
1238
2044
|
|
|
1239
|
-
prae init [--pack <file>] [--profile <dir>] [--ledger <file>]
|
|
2045
|
+
prae init [--pack <file> | --pack-dir <dir>] [--profile <dir>] [--ledger <file>]
|
|
1240
2046
|
(no --pack mounts the built-in baseline of 8 high-risk rules)
|
|
1241
2047
|
[--approver <name>] [--link] [--shadow] [--apply]
|
|
1242
2048
|
Compose the overlay row that mounts PRAE with a pack. Prints the
|
|
1243
2049
|
diff; writes only with --apply. --link points the profile at the
|
|
1244
2050
|
pack file instead of copying it in, so edits hot-reload; --shadow
|
|
1245
2051
|
mounts in shadow mode: every verdict sealed, every call let through.
|
|
2052
|
+
--pack-dir adds the workspace's own packs (e.g. .prae/packs) beside
|
|
2053
|
+
that pack, loaded and watched as a set; a set that would not load is
|
|
2054
|
+
not mounted (see \`prae check <dir>\`).
|
|
1246
2055
|
`;
|
|
1247
2056
|
function fail(message) {
|
|
1248
2057
|
console.error(`prae: ${message}`);
|
|
@@ -1263,7 +2072,25 @@ async function loadPack(path) {
|
|
|
1263
2072
|
}
|
|
1264
2073
|
return result.pack;
|
|
1265
2074
|
}
|
|
1266
|
-
async function
|
|
2075
|
+
async function checkDir(dir, keyPath) {
|
|
2076
|
+
let key;
|
|
2077
|
+
if (keyPath !== void 0) try {
|
|
2078
|
+
key = await readFile(keyPath, "utf8");
|
|
2079
|
+
} catch {
|
|
2080
|
+
return fail(`cannot read ${keyPath}`);
|
|
2081
|
+
}
|
|
2082
|
+
const set = loadPackDir(dir, key);
|
|
2083
|
+
for (const p of set.packs) {
|
|
2084
|
+
console.log(`ok ${p.file.split("/").at(-1)} · ${p.pack.pack} ${p.pack.version} · ${p.pack.rules.length} rules · sha256 ${p.sha256.slice(0, 12)}${p.signedBy === void 0 ? "" : ` · signed ${p.signedBy}`}`);
|
|
2085
|
+
for (const rule of p.pack.rules) console.log(` ${rule.id.padEnd(14)} ${(rule.effect.kind === "ask" ? "HOLD" : rule.effect.kind.toUpperCase()).padEnd(8)} ${rule.title ?? rule.effect.because}`);
|
|
2086
|
+
}
|
|
2087
|
+
for (const r of set.rejected) console.error(`REJECTED ${r.file.split("/").at(-1)}\n ${r.reason}`);
|
|
2088
|
+
if (set.packs.length === 0) console.error("\nNo valid pack: the gate would refuse every call until one loads.");
|
|
2089
|
+
if (set.rejected.length > 0) process.exit(1);
|
|
2090
|
+
console.log(`\n${set.packs.length} pack${set.packs.length === 1 ? "" : "s"} valid · in force as ${set.effective.pack} ${set.effective.version}`);
|
|
2091
|
+
}
|
|
2092
|
+
async function check(path, argv = []) {
|
|
2093
|
+
if (existsSync(path) && statSync(path).isDirectory()) return checkDir(path, flag(argv, "policy-key"));
|
|
1267
2094
|
const pack = await loadPack(path);
|
|
1268
2095
|
console.log(`${pack.pack} ${pack.version} — valid`);
|
|
1269
2096
|
const baseline = pack.baseline?.capabilities;
|
|
@@ -1324,13 +2151,13 @@ async function scanFile(path, packPath, json) {
|
|
|
1324
2151
|
* not come up inspecting nothing.
|
|
1325
2152
|
*/
|
|
1326
2153
|
async function mcp(root, packPath, ledgerPath) {
|
|
1327
|
-
const { serveMcp } = await import("./mcp-
|
|
2154
|
+
const { serveMcp } = await import("./mcp-BCXbpqu4.js");
|
|
1328
2155
|
const { resolve: resolve$1 } = await import("node:path");
|
|
1329
2156
|
const bundled = [join(import.meta.dirname, "secret-catcher.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "secret-catcher.prae.json")].find((candidate) => existsSync(candidate));
|
|
1330
2157
|
const chosen = packPath ?? bundled;
|
|
1331
2158
|
if (chosen === void 0) return fail("mcp needs --pack <file>; no bundled secret-catcher found");
|
|
1332
2159
|
const pack = await loadPack(chosen);
|
|
1333
|
-
const { Ledger: Ledger$1 } = await import("./src-
|
|
2160
|
+
const { Ledger: Ledger$1 } = await import("./src-7X6dO7gC.js");
|
|
1334
2161
|
const ledger = ledgerPath === void 0 ? void 0 : Ledger$1.open(ledgerPath);
|
|
1335
2162
|
console.error(`prae mcp: serving ${resolve$1(root)} under ${pack.pack} ${pack.version}` + (ledgerPath === void 0 ? " (no ledger)" : ` · ledger ${ledgerPath}`));
|
|
1336
2163
|
await serveMcp({
|
|
@@ -1382,7 +2209,7 @@ async function explain(path, tool, pairs) {
|
|
|
1382
2209
|
* a key that arrived beside it is checked against nothing.
|
|
1383
2210
|
*/
|
|
1384
2211
|
async function keygen(dir) {
|
|
1385
|
-
const { generateKeypair, fingerprint: fingerprint$1 } = await import("./src-
|
|
2212
|
+
const { generateKeypair, fingerprint: fingerprint$1 } = await import("./src-7X6dO7gC.js");
|
|
1386
2213
|
const { writeFileSync: writeFileSync$1 } = await import("node:fs");
|
|
1387
2214
|
const keys = generateKeypair();
|
|
1388
2215
|
const priv = join(dir, "prae-signing.pem");
|
|
@@ -1397,12 +2224,77 @@ async function keygen(dir) {
|
|
|
1397
2224
|
console.log("receipts, through a channel they trust. Point the plugin at the");
|
|
1398
2225
|
console.log("private key with `signingKey` to sign every row.");
|
|
1399
2226
|
}
|
|
2227
|
+
/**
|
|
2228
|
+
* A sealed keypair for a human authority (PRA-106/107): written as
|
|
2229
|
+
* `<role>.pub.pem` and `<role>.key.sealed.pem`. The passphrase is never an
|
|
2230
|
+
* argument — arguments land in process listings and shell history.
|
|
2231
|
+
*/
|
|
2232
|
+
async function keygenSealed(role, dir) {
|
|
2233
|
+
if (role !== "operator" && role !== "policy" && role !== "ledger") fail("--role is operator, policy or ledger");
|
|
2234
|
+
const stem = role === "ledger" ? "prae-signing" : role;
|
|
2235
|
+
const { sealedKeypair, fingerprint: fingerprint$1 } = await import("./src-7X6dO7gC.js");
|
|
2236
|
+
const { writeFileSync: writeFileSync$1 } = await import("node:fs");
|
|
2237
|
+
const keys = sealedKeypair(process.env.PRAE_KEY_PASSPHRASE ?? await askHidden("passphrase: "));
|
|
2238
|
+
const pub = join(dir, `${stem}.pub.pem`);
|
|
2239
|
+
const sealed = join(dir, `${stem}.key.sealed.pem`);
|
|
2240
|
+
writeFileSync$1(sealed, keys.sealedPrivateKey, { mode: 384 });
|
|
2241
|
+
writeFileSync$1(pub, keys.publicKey);
|
|
2242
|
+
console.log(`sealed ${sealed} (useless without the passphrase)`);
|
|
2243
|
+
console.log(`public ${pub}`);
|
|
2244
|
+
console.log(`fingerprint ${fingerprint$1(keys.publicKey)}`);
|
|
2245
|
+
}
|
|
2246
|
+
/** A detached signature over a pack's bytes (PRA-107). */
|
|
2247
|
+
async function signPack(packPath, keyPath) {
|
|
2248
|
+
const { signBytes, unsealPrivateKey, fingerprint: fingerprint$1 } = await import("./src-7X6dO7gC.js");
|
|
2249
|
+
const { createPublicKey: createPublicKey$1 } = await import("node:crypto");
|
|
2250
|
+
const bytes = readFileSync(packPath);
|
|
2251
|
+
const parsed = parsePack(bytes.toString("utf8"));
|
|
2252
|
+
if (!parsed.ok) fail(`${packPath} does not parse; nothing signed`);
|
|
2253
|
+
const passphrase = process.env.PRAE_KEY_PASSPHRASE ?? await askHidden("passphrase: ");
|
|
2254
|
+
let key;
|
|
2255
|
+
try {
|
|
2256
|
+
key = unsealPrivateKey(readFileSync(keyPath, "utf8"), passphrase);
|
|
2257
|
+
} catch {
|
|
2258
|
+
fail("wrong passphrase, or not a sealed key");
|
|
2259
|
+
}
|
|
2260
|
+
await writeFile(`${packPath}.sig`, `${signBytes(bytes, key)}\n`);
|
|
2261
|
+
const pub = createPublicKey$1(key).export({
|
|
2262
|
+
type: "spki",
|
|
2263
|
+
format: "pem"
|
|
2264
|
+
});
|
|
2265
|
+
console.log(`signed ${packPath}.sig (${parsed.pack.pack} ${parsed.pack.version})`);
|
|
2266
|
+
console.log(`policy key ${fingerprint$1(pub.toString())}`);
|
|
2267
|
+
}
|
|
2268
|
+
/** Read a line from the terminal without echoing it. */
|
|
2269
|
+
function askHidden(prompt) {
|
|
2270
|
+
const input = process.stdin;
|
|
2271
|
+
if (!input.isTTY) fail("no terminal to ask on: set PRAE_KEY_PASSPHRASE");
|
|
2272
|
+
process.stdout.write(prompt);
|
|
2273
|
+
input.setRawMode(true);
|
|
2274
|
+
input.resume();
|
|
2275
|
+
input.setEncoding("utf8");
|
|
2276
|
+
return new Promise((done) => {
|
|
2277
|
+
let typed = "";
|
|
2278
|
+
const onData = (ch) => {
|
|
2279
|
+
if (ch === "\r" || ch === "\n") {
|
|
2280
|
+
input.setRawMode(false);
|
|
2281
|
+
input.pause();
|
|
2282
|
+
input.off("data", onData);
|
|
2283
|
+
process.stdout.write("\n");
|
|
2284
|
+
done(typed);
|
|
2285
|
+
} else if (ch === "") process.exit(130);
|
|
2286
|
+
else if (ch === "") typed = typed.slice(0, -1);
|
|
2287
|
+
else typed += ch;
|
|
2288
|
+
};
|
|
2289
|
+
input.on("data", onData);
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
1400
2292
|
async function receipt(path, seq, keyPath, out) {
|
|
1401
|
-
const { Ledger: Ledger$1, makeReceipt, fingerprint: fingerprint$1 } = await import("./src-
|
|
2293
|
+
const { Ledger: Ledger$1, makeReceipt, fingerprint: fingerprint$1 } = await import("./src-7X6dO7gC.js");
|
|
1402
2294
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1403
|
-
const { createPublicKey } = await import("node:crypto");
|
|
2295
|
+
const { createPublicKey: createPublicKey$1 } = await import("node:crypto");
|
|
1404
2296
|
const privateKey = readFileSync$1(keyPath, "utf8");
|
|
1405
|
-
const publicKey = createPublicKey(privateKey).export({
|
|
2297
|
+
const publicKey = createPublicKey$1(privateKey).export({
|
|
1406
2298
|
type: "spki",
|
|
1407
2299
|
format: "pem"
|
|
1408
2300
|
}).toString();
|
|
@@ -1428,7 +2320,7 @@ async function receipt(path, seq, keyPath, out) {
|
|
|
1428
2320
|
* file and sets the exit code, so scripts can branch on it.
|
|
1429
2321
|
*/
|
|
1430
2322
|
async function verifyReceiptCmd(path) {
|
|
1431
|
-
const { checkReceipt } = await import("./receipt-check-
|
|
2323
|
+
const { checkReceipt } = await import("./receipt-check-DQK_gVPc.js");
|
|
1432
2324
|
let parsed;
|
|
1433
2325
|
try {
|
|
1434
2326
|
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
@@ -1459,7 +2351,7 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
1459
2351
|
} catch (error) {
|
|
1460
2352
|
return fail(`${suitePath} is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
1461
2353
|
}
|
|
1462
|
-
const { parseSuite, renderSuite, runSuite } = await import("./test-run-
|
|
2354
|
+
const { parseSuite, renderSuite, runSuite } = await import("./test-run-CCDgJ0Dp.js");
|
|
1463
2355
|
const suite = parseSuite(parsed);
|
|
1464
2356
|
if (!suite.ok) {
|
|
1465
2357
|
console.error(`prae: ${suitePath} is not a valid test suite`);
|
|
@@ -1486,7 +2378,7 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
1486
2378
|
else for (const line of renderSuite(result)) console.log(line);
|
|
1487
2379
|
if (!result.ok) process.exit(1);
|
|
1488
2380
|
}
|
|
1489
|
-
async function verify(path, keyPath, checkpointPath) {
|
|
2381
|
+
async function verify$1(path, keyPath, checkpointPath) {
|
|
1490
2382
|
let ledger;
|
|
1491
2383
|
try {
|
|
1492
2384
|
ledger = Ledger.open(path);
|
|
@@ -1499,14 +2391,14 @@ async function verify(path, keyPath, checkpointPath) {
|
|
|
1499
2391
|
const last = entries.at(-1);
|
|
1500
2392
|
console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
|
|
1501
2393
|
if (checkpointPath !== void 0) {
|
|
1502
|
-
const { checkCheckpoint } = await import("./checkpoint-
|
|
2394
|
+
const { checkCheckpoint } = await import("./checkpoint-C_q_Rpa5.js");
|
|
1503
2395
|
const publicKey = keyPath === void 0 ? void 0 : readFileSync(keyPath, "utf8");
|
|
1504
2396
|
const verdict = checkCheckpoint(entries, JSON.parse(readFileSync(checkpointPath, "utf8")), publicKey);
|
|
1505
2397
|
for (const line of verdict.lines) (verdict.ok ? console.log : console.error)(`checkpoint: ${line}`);
|
|
1506
2398
|
if (!verdict.ok) process.exitCode = 1;
|
|
1507
2399
|
}
|
|
1508
2400
|
if (keyPath !== void 0) {
|
|
1509
|
-
const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-
|
|
2401
|
+
const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-7X6dO7gC.js");
|
|
1510
2402
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1511
2403
|
const publicKey = readFileSync$1(keyPath, "utf8");
|
|
1512
2404
|
let signed = 0;
|
|
@@ -1682,7 +2574,7 @@ async function consoleOut(path, argv) {
|
|
|
1682
2574
|
const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
|
|
1683
2575
|
import("./src-d4c_n6wd.js"),
|
|
1684
2576
|
import("./src-CBGZVvt7.js"),
|
|
1685
|
-
import("./instructions-
|
|
2577
|
+
import("./instructions-ya0LVnZy.js")
|
|
1686
2578
|
]);
|
|
1687
2579
|
const read = ingest(instructionsDir);
|
|
1688
2580
|
instructionsView = renderInstructions(data, read, extract(read.statements));
|
|
@@ -1733,14 +2625,29 @@ async function init(argv) {
|
|
|
1733
2625
|
}
|
|
1734
2626
|
const pack = await loadPack(packPath);
|
|
1735
2627
|
const source = await readFile(packPath, "utf8");
|
|
2628
|
+
const link = argv.includes("--link");
|
|
2629
|
+
const builtin = link ? { packFile: resolve(packPath) } : { pack: source };
|
|
2630
|
+
const builtinLine = `${pack.pack} ${pack.version} · ${pack.rules.length} rules${link ? ` · linked, edits hot-reload (${resolve(packPath)})` : ""}`;
|
|
2631
|
+
const packDirFlag = flag(argv, "pack-dir");
|
|
2632
|
+
if (packDirFlag === void 0) return writeRow(argv, builtin, builtinLine);
|
|
2633
|
+
const packDir = resolve(packDirFlag);
|
|
2634
|
+
const set = loadPackDir(packDir, void 0, void 0, readPackFile(packPath, void 0, "builtin"));
|
|
2635
|
+
for (const r of set.rejected) console.error(`REJECTED ${r.file.split("/").at(-1)}: ${r.reason}`);
|
|
2636
|
+
if (set.rejected.length > 0) fail(`not mounting ${packDir}: fix the packs above (prae check ${packDir})`);
|
|
2637
|
+
const custom = set.packs.filter((p) => p.origin === "workspace");
|
|
2638
|
+
return writeRow(argv, {
|
|
2639
|
+
...builtin,
|
|
2640
|
+
packDir
|
|
2641
|
+
}, `${builtinLine}\n + ${custom.map((p) => `${p.pack.pack} ${p.pack.version}`).join(", ")} · workspace, watched (${packDir})`);
|
|
2642
|
+
}
|
|
2643
|
+
async function writeRow(argv, source, packLine) {
|
|
1736
2644
|
const profileDir = flag(argv, "profile");
|
|
1737
2645
|
const profile = profileDir === void 0 ? defaultProfile() : profileAt(profileDir);
|
|
1738
2646
|
const ledger = flag(argv, "ledger");
|
|
1739
2647
|
const approver = flag(argv, "approver");
|
|
1740
|
-
const link = argv.includes("--link");
|
|
1741
2648
|
const shadow = argv.includes("--shadow");
|
|
1742
2649
|
const row$1 = praeRow({
|
|
1743
|
-
...
|
|
2650
|
+
...source,
|
|
1744
2651
|
...shadow ? { mode: "shadow" } : {},
|
|
1745
2652
|
...ledger === void 0 ? {} : { ledger },
|
|
1746
2653
|
...approver === void 0 ? {} : { approver }
|
|
@@ -1753,7 +2660,7 @@ async function init(argv) {
|
|
|
1753
2660
|
}
|
|
1754
2661
|
console.log(`profile ${profile.dir}`);
|
|
1755
2662
|
console.log(`overlay ${plan.patchPath}`);
|
|
1756
|
-
console.log(`pack ${
|
|
2663
|
+
console.log(`pack ${packLine}`);
|
|
1757
2664
|
if (shadow) console.log("mode shadow · every verdict sealed, every call proceeds");
|
|
1758
2665
|
if (ledger !== void 0) console.log(`ledger ${ledger}`);
|
|
1759
2666
|
if (plan.findings.length > 0) {
|
|
@@ -1780,8 +2687,55 @@ async function init(argv) {
|
|
|
1780
2687
|
return fail(error instanceof Error ? error.message : String(error));
|
|
1781
2688
|
}
|
|
1782
2689
|
}
|
|
2690
|
+
/** `prae chain`: the Attack Chain Integrity Matrix, from the record alone (PRA-128). */
|
|
2691
|
+
async function chainMatrix(path, argv) {
|
|
2692
|
+
const packPath = flag(argv, "pack");
|
|
2693
|
+
if (packPath === void 0) fail("chain needs --pack <pack.json>");
|
|
2694
|
+
const pack = await loadPack(packPath);
|
|
2695
|
+
let ledger;
|
|
2696
|
+
try {
|
|
2697
|
+
ledger = Ledger.open(path);
|
|
2698
|
+
} catch (error) {
|
|
2699
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
2700
|
+
}
|
|
2701
|
+
const entries = ledger.read();
|
|
2702
|
+
const chainOk = ledger.verify().ok;
|
|
2703
|
+
const guardiansPath = flag(argv, "guardians");
|
|
2704
|
+
const catalogue = guardiansPath === void 0 ? [] : JSON.parse(readFileSync(guardiansPath, "utf8")).guardians;
|
|
2705
|
+
const guardianOf = (rule) => rule === void 0 ? void 0 : catalogue.find((g) => g.rules.includes(rule) || (g.armed ?? []).includes(rule))?.name;
|
|
2706
|
+
const keyPath = flag(argv, "key");
|
|
2707
|
+
const { verifySignature: verifySignature$1 } = await import("./src-7X6dO7gC.js");
|
|
2708
|
+
const publicKey = keyPath === void 0 ? void 0 : readFileSync(keyPath, "utf8");
|
|
2709
|
+
const matrix = integrityMatrix(entries, pack, guardianOf).map((m) => {
|
|
2710
|
+
const entry = entries.find((e) => e.seq === m.seq);
|
|
2711
|
+
return {
|
|
2712
|
+
...m,
|
|
2713
|
+
verified: publicKey === void 0 || entry?.sig === void 0 ? void 0 : verifySignature$1(entry, entry.sig, publicKey)
|
|
2714
|
+
};
|
|
2715
|
+
});
|
|
2716
|
+
const sigFailed = matrix.some((m) => m.verified === false);
|
|
2717
|
+
if (argv.includes("--json")) console.log(JSON.stringify({
|
|
2718
|
+
chain: chainOk,
|
|
2719
|
+
rows: matrix
|
|
2720
|
+
}, null, 2));
|
|
2721
|
+
else {
|
|
2722
|
+
for (const m of matrix) {
|
|
2723
|
+
console.log(`#${m.seq} ${m.stage}`);
|
|
2724
|
+
console.log(` attempted ${m.action}${m.target ? ` ${m.target}` : ""}`);
|
|
2725
|
+
console.log(` capability ${m.capability ?? "—"}`);
|
|
2726
|
+
console.log(` guardian ${m.guardian ?? "—"}`);
|
|
2727
|
+
console.log(` rule ${m.rule === void 0 ? "— (no rule matched)" : m.ruleRole === "recorded" ? `${m.rule} (matched and recorded; did not decide)` : m.rule}`);
|
|
2728
|
+
console.log(` decision ${m.decision} · ${m.execution}`);
|
|
2729
|
+
console.log(` basis ${m.basis}`);
|
|
2730
|
+
if (m.mismatch !== void 0) console.log(` mismatch row #${m.mismatch} (sealed link)`);
|
|
2731
|
+
console.log(` evidence hash ${m.hash.slice(0, 16)} · prev ${m.prev.slice(0, 16)} · ${m.verified === void 0 ? m.signed ? "signed" : "unsigned" : m.verified ? "signature verifies" : "SIGNATURE FAILS"}`);
|
|
2732
|
+
}
|
|
2733
|
+
console.log(`\n${matrix.length} attack-chain rows · chain ${chainOk ? "intact" : "BROKEN"}${publicKey === void 0 ? "" : sigFailed ? " · signatures FAIL" : " · signatures verify"}`);
|
|
2734
|
+
}
|
|
2735
|
+
if (!chainOk || sigFailed) process.exitCode = 1;
|
|
2736
|
+
}
|
|
1783
2737
|
async function checkpoint(path, keyPath, out) {
|
|
1784
|
-
const { makeCheckpoint } = await import("./checkpoint-
|
|
2738
|
+
const { makeCheckpoint } = await import("./checkpoint-C_q_Rpa5.js");
|
|
1785
2739
|
let ledger;
|
|
1786
2740
|
try {
|
|
1787
2741
|
ledger = Ledger.open(path);
|
|
@@ -1839,8 +2793,8 @@ async function main(argv) {
|
|
|
1839
2793
|
switch (command) {
|
|
1840
2794
|
case "check": {
|
|
1841
2795
|
const path = rest[0];
|
|
1842
|
-
if (path === void 0) fail("check needs a pack file");
|
|
1843
|
-
return check(path);
|
|
2796
|
+
if (path === void 0) fail("check needs a pack file or directory");
|
|
2797
|
+
return check(path, rest);
|
|
1844
2798
|
}
|
|
1845
2799
|
case "explain": {
|
|
1846
2800
|
const [path, tool, ...pairs] = rest;
|
|
@@ -1857,7 +2811,17 @@ async function main(argv) {
|
|
|
1857
2811
|
if (root === void 0) fail("mcp needs --root <dir>; serving without a boundary is not a gate");
|
|
1858
2812
|
return mcp(root, flag(rest, "pack"), flag(rest, "ledger"));
|
|
1859
2813
|
}
|
|
1860
|
-
case "keygen":
|
|
2814
|
+
case "keygen": {
|
|
2815
|
+
const role = flag(rest, "role");
|
|
2816
|
+
if (role !== void 0) return keygenSealed(role, flag(rest, "out") ?? ".");
|
|
2817
|
+
return keygen(flag(rest, "out") ?? ".");
|
|
2818
|
+
}
|
|
2819
|
+
case "sign-pack": {
|
|
2820
|
+
const [path] = rest;
|
|
2821
|
+
const key = flag(rest, "key");
|
|
2822
|
+
if (path === void 0 || key === void 0) fail("sign-pack needs <pack.json> --key <policy.key.sealed.pem>");
|
|
2823
|
+
return signPack(path, key);
|
|
2824
|
+
}
|
|
1861
2825
|
case "receipt": {
|
|
1862
2826
|
const [path, seq] = rest;
|
|
1863
2827
|
const key = flag(rest, "key");
|
|
@@ -1873,7 +2837,12 @@ async function main(argv) {
|
|
|
1873
2837
|
case "verify": {
|
|
1874
2838
|
const path = rest[0];
|
|
1875
2839
|
if (path === void 0) fail("verify needs a ledger file");
|
|
1876
|
-
return verify(path, flag(rest, "key"), flag(rest, "checkpoint"));
|
|
2840
|
+
return verify$1(path, flag(rest, "key"), flag(rest, "checkpoint"));
|
|
2841
|
+
}
|
|
2842
|
+
case "chain": {
|
|
2843
|
+
const path = rest[0];
|
|
2844
|
+
if (path === void 0) fail("chain needs a ledger file");
|
|
2845
|
+
return chainMatrix(path, rest);
|
|
1877
2846
|
}
|
|
1878
2847
|
case "checkpoint": {
|
|
1879
2848
|
const path = rest[0];
|