prae-cli 0.9.1 → 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-BWSuhZ5h.js → checkpoint-C_q_Rpa5.js} +1 -1
- package/lib/{cli-7yLG89gV.js → cli-B_B6-bUZ.js} +934 -56
- 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-Bg5puOen.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-CD-W1wXs.js → receipt-check-DQK_gVPc.js} +1 -1
- package/lib/src-7X6dO7gC.js +3 -0
- package/lib/{src-D7g4lvaY.js → src-LA7NdLKb.js} +8 -1
- package/lib/{test-run-XNIRHKOu.js → test-run-CCDgJ0Dp.js} +1 -1
- package/lib/{views-CIg2ZNG0.js → views-D4UEfbzt.js} +106 -2
- package/package.json +5 -4
- package/lib/src-d82lSKVQ.js +0 -3
|
@@ -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,33 +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
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
+
};
|
|
1018
1790
|
}
|
|
1019
1791
|
/**
|
|
1020
1792
|
* The record over HTTP, for the Command Center (apps/console): the same
|
|
@@ -1071,6 +1843,10 @@ function handleApi(req, res, record) {
|
|
|
1071
1843
|
json(complianceOf(record));
|
|
1072
1844
|
return true;
|
|
1073
1845
|
}
|
|
1846
|
+
if (route === "/api/packs") {
|
|
1847
|
+
json(packsOf(record));
|
|
1848
|
+
return true;
|
|
1849
|
+
}
|
|
1074
1850
|
if (route === "/api/redteam") {
|
|
1075
1851
|
json(readRedteam(dir, files));
|
|
1076
1852
|
return true;
|
|
@@ -1169,6 +1945,12 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1169
1945
|
prae check <pack.json>
|
|
1170
1946
|
Validate a policy pack and summarise what it enforces.
|
|
1171
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
|
+
|
|
1172
1954
|
prae explain <pack.json> <tool> [key=value ...]
|
|
1173
1955
|
Judge one call against the pack and print the trace. Nothing is executed.
|
|
1174
1956
|
|
|
@@ -1195,6 +1977,14 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1195
1977
|
--checkpoint, also check that nothing before that checkpoint's head
|
|
1196
1978
|
has been removed or rewritten since it was taken.
|
|
1197
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
|
+
|
|
1198
1988
|
prae checkpoint <ledger.jsonl> [--key <private.pem>] [--out <file>]
|
|
1199
1989
|
Record the ledger's length and head, signed, to keep somewhere the
|
|
1200
1990
|
ledger's machine cannot write. A chain proves no row inside the file
|
|
@@ -1252,13 +2042,16 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1252
2042
|
Read the instruction files at <dir> and propose the rules they imply.
|
|
1253
2043
|
Proposals only: an existing pack is never modified.
|
|
1254
2044
|
|
|
1255
|
-
prae init [--pack <file>] [--profile <dir>] [--ledger <file>]
|
|
2045
|
+
prae init [--pack <file> | --pack-dir <dir>] [--profile <dir>] [--ledger <file>]
|
|
1256
2046
|
(no --pack mounts the built-in baseline of 8 high-risk rules)
|
|
1257
2047
|
[--approver <name>] [--link] [--shadow] [--apply]
|
|
1258
2048
|
Compose the overlay row that mounts PRAE with a pack. Prints the
|
|
1259
2049
|
diff; writes only with --apply. --link points the profile at the
|
|
1260
2050
|
pack file instead of copying it in, so edits hot-reload; --shadow
|
|
1261
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>\`).
|
|
1262
2055
|
`;
|
|
1263
2056
|
function fail(message) {
|
|
1264
2057
|
console.error(`prae: ${message}`);
|
|
@@ -1279,7 +2072,25 @@ async function loadPack(path) {
|
|
|
1279
2072
|
}
|
|
1280
2073
|
return result.pack;
|
|
1281
2074
|
}
|
|
1282
|
-
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"));
|
|
1283
2094
|
const pack = await loadPack(path);
|
|
1284
2095
|
console.log(`${pack.pack} ${pack.version} — valid`);
|
|
1285
2096
|
const baseline = pack.baseline?.capabilities;
|
|
@@ -1340,13 +2151,13 @@ async function scanFile(path, packPath, json) {
|
|
|
1340
2151
|
* not come up inspecting nothing.
|
|
1341
2152
|
*/
|
|
1342
2153
|
async function mcp(root, packPath, ledgerPath) {
|
|
1343
|
-
const { serveMcp } = await import("./mcp-
|
|
2154
|
+
const { serveMcp } = await import("./mcp-BCXbpqu4.js");
|
|
1344
2155
|
const { resolve: resolve$1 } = await import("node:path");
|
|
1345
2156
|
const bundled = [join(import.meta.dirname, "secret-catcher.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "secret-catcher.prae.json")].find((candidate) => existsSync(candidate));
|
|
1346
2157
|
const chosen = packPath ?? bundled;
|
|
1347
2158
|
if (chosen === void 0) return fail("mcp needs --pack <file>; no bundled secret-catcher found");
|
|
1348
2159
|
const pack = await loadPack(chosen);
|
|
1349
|
-
const { Ledger: Ledger$1 } = await import("./src-
|
|
2160
|
+
const { Ledger: Ledger$1 } = await import("./src-7X6dO7gC.js");
|
|
1350
2161
|
const ledger = ledgerPath === void 0 ? void 0 : Ledger$1.open(ledgerPath);
|
|
1351
2162
|
console.error(`prae mcp: serving ${resolve$1(root)} under ${pack.pack} ${pack.version}` + (ledgerPath === void 0 ? " (no ledger)" : ` · ledger ${ledgerPath}`));
|
|
1352
2163
|
await serveMcp({
|
|
@@ -1398,7 +2209,7 @@ async function explain(path, tool, pairs) {
|
|
|
1398
2209
|
* a key that arrived beside it is checked against nothing.
|
|
1399
2210
|
*/
|
|
1400
2211
|
async function keygen(dir) {
|
|
1401
|
-
const { generateKeypair, fingerprint: fingerprint$1 } = await import("./src-
|
|
2212
|
+
const { generateKeypair, fingerprint: fingerprint$1 } = await import("./src-7X6dO7gC.js");
|
|
1402
2213
|
const { writeFileSync: writeFileSync$1 } = await import("node:fs");
|
|
1403
2214
|
const keys = generateKeypair();
|
|
1404
2215
|
const priv = join(dir, "prae-signing.pem");
|
|
@@ -1421,7 +2232,7 @@ async function keygen(dir) {
|
|
|
1421
2232
|
async function keygenSealed(role, dir) {
|
|
1422
2233
|
if (role !== "operator" && role !== "policy" && role !== "ledger") fail("--role is operator, policy or ledger");
|
|
1423
2234
|
const stem = role === "ledger" ? "prae-signing" : role;
|
|
1424
|
-
const { sealedKeypair, fingerprint: fingerprint$1 } = await import("./src-
|
|
2235
|
+
const { sealedKeypair, fingerprint: fingerprint$1 } = await import("./src-7X6dO7gC.js");
|
|
1425
2236
|
const { writeFileSync: writeFileSync$1 } = await import("node:fs");
|
|
1426
2237
|
const keys = sealedKeypair(process.env.PRAE_KEY_PASSPHRASE ?? await askHidden("passphrase: "));
|
|
1427
2238
|
const pub = join(dir, `${stem}.pub.pem`);
|
|
@@ -1434,8 +2245,8 @@ async function keygenSealed(role, dir) {
|
|
|
1434
2245
|
}
|
|
1435
2246
|
/** A detached signature over a pack's bytes (PRA-107). */
|
|
1436
2247
|
async function signPack(packPath, keyPath) {
|
|
1437
|
-
const { signBytes, unsealPrivateKey, fingerprint: fingerprint$1 } = await import("./src-
|
|
1438
|
-
const { createPublicKey } = await import("node:crypto");
|
|
2248
|
+
const { signBytes, unsealPrivateKey, fingerprint: fingerprint$1 } = await import("./src-7X6dO7gC.js");
|
|
2249
|
+
const { createPublicKey: createPublicKey$1 } = await import("node:crypto");
|
|
1439
2250
|
const bytes = readFileSync(packPath);
|
|
1440
2251
|
const parsed = parsePack(bytes.toString("utf8"));
|
|
1441
2252
|
if (!parsed.ok) fail(`${packPath} does not parse; nothing signed`);
|
|
@@ -1447,7 +2258,7 @@ async function signPack(packPath, keyPath) {
|
|
|
1447
2258
|
fail("wrong passphrase, or not a sealed key");
|
|
1448
2259
|
}
|
|
1449
2260
|
await writeFile(`${packPath}.sig`, `${signBytes(bytes, key)}\n`);
|
|
1450
|
-
const pub = createPublicKey(key).export({
|
|
2261
|
+
const pub = createPublicKey$1(key).export({
|
|
1451
2262
|
type: "spki",
|
|
1452
2263
|
format: "pem"
|
|
1453
2264
|
});
|
|
@@ -1479,11 +2290,11 @@ function askHidden(prompt) {
|
|
|
1479
2290
|
});
|
|
1480
2291
|
}
|
|
1481
2292
|
async function receipt(path, seq, keyPath, out) {
|
|
1482
|
-
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");
|
|
1483
2294
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1484
|
-
const { createPublicKey } = await import("node:crypto");
|
|
2295
|
+
const { createPublicKey: createPublicKey$1 } = await import("node:crypto");
|
|
1485
2296
|
const privateKey = readFileSync$1(keyPath, "utf8");
|
|
1486
|
-
const publicKey = createPublicKey(privateKey).export({
|
|
2297
|
+
const publicKey = createPublicKey$1(privateKey).export({
|
|
1487
2298
|
type: "spki",
|
|
1488
2299
|
format: "pem"
|
|
1489
2300
|
}).toString();
|
|
@@ -1509,7 +2320,7 @@ async function receipt(path, seq, keyPath, out) {
|
|
|
1509
2320
|
* file and sets the exit code, so scripts can branch on it.
|
|
1510
2321
|
*/
|
|
1511
2322
|
async function verifyReceiptCmd(path) {
|
|
1512
|
-
const { checkReceipt } = await import("./receipt-check-
|
|
2323
|
+
const { checkReceipt } = await import("./receipt-check-DQK_gVPc.js");
|
|
1513
2324
|
let parsed;
|
|
1514
2325
|
try {
|
|
1515
2326
|
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
@@ -1540,7 +2351,7 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
1540
2351
|
} catch (error) {
|
|
1541
2352
|
return fail(`${suitePath} is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
1542
2353
|
}
|
|
1543
|
-
const { parseSuite, renderSuite, runSuite } = await import("./test-run-
|
|
2354
|
+
const { parseSuite, renderSuite, runSuite } = await import("./test-run-CCDgJ0Dp.js");
|
|
1544
2355
|
const suite = parseSuite(parsed);
|
|
1545
2356
|
if (!suite.ok) {
|
|
1546
2357
|
console.error(`prae: ${suitePath} is not a valid test suite`);
|
|
@@ -1567,7 +2378,7 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
1567
2378
|
else for (const line of renderSuite(result)) console.log(line);
|
|
1568
2379
|
if (!result.ok) process.exit(1);
|
|
1569
2380
|
}
|
|
1570
|
-
async function verify(path, keyPath, checkpointPath) {
|
|
2381
|
+
async function verify$1(path, keyPath, checkpointPath) {
|
|
1571
2382
|
let ledger;
|
|
1572
2383
|
try {
|
|
1573
2384
|
ledger = Ledger.open(path);
|
|
@@ -1580,14 +2391,14 @@ async function verify(path, keyPath, checkpointPath) {
|
|
|
1580
2391
|
const last = entries.at(-1);
|
|
1581
2392
|
console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
|
|
1582
2393
|
if (checkpointPath !== void 0) {
|
|
1583
|
-
const { checkCheckpoint } = await import("./checkpoint-
|
|
2394
|
+
const { checkCheckpoint } = await import("./checkpoint-C_q_Rpa5.js");
|
|
1584
2395
|
const publicKey = keyPath === void 0 ? void 0 : readFileSync(keyPath, "utf8");
|
|
1585
2396
|
const verdict = checkCheckpoint(entries, JSON.parse(readFileSync(checkpointPath, "utf8")), publicKey);
|
|
1586
2397
|
for (const line of verdict.lines) (verdict.ok ? console.log : console.error)(`checkpoint: ${line}`);
|
|
1587
2398
|
if (!verdict.ok) process.exitCode = 1;
|
|
1588
2399
|
}
|
|
1589
2400
|
if (keyPath !== void 0) {
|
|
1590
|
-
const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-
|
|
2401
|
+
const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-7X6dO7gC.js");
|
|
1591
2402
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1592
2403
|
const publicKey = readFileSync$1(keyPath, "utf8");
|
|
1593
2404
|
let signed = 0;
|
|
@@ -1763,7 +2574,7 @@ async function consoleOut(path, argv) {
|
|
|
1763
2574
|
const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
|
|
1764
2575
|
import("./src-d4c_n6wd.js"),
|
|
1765
2576
|
import("./src-CBGZVvt7.js"),
|
|
1766
|
-
import("./instructions-
|
|
2577
|
+
import("./instructions-ya0LVnZy.js")
|
|
1767
2578
|
]);
|
|
1768
2579
|
const read = ingest(instructionsDir);
|
|
1769
2580
|
instructionsView = renderInstructions(data, read, extract(read.statements));
|
|
@@ -1814,14 +2625,29 @@ async function init(argv) {
|
|
|
1814
2625
|
}
|
|
1815
2626
|
const pack = await loadPack(packPath);
|
|
1816
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) {
|
|
1817
2644
|
const profileDir = flag(argv, "profile");
|
|
1818
2645
|
const profile = profileDir === void 0 ? defaultProfile() : profileAt(profileDir);
|
|
1819
2646
|
const ledger = flag(argv, "ledger");
|
|
1820
2647
|
const approver = flag(argv, "approver");
|
|
1821
|
-
const link = argv.includes("--link");
|
|
1822
2648
|
const shadow = argv.includes("--shadow");
|
|
1823
2649
|
const row$1 = praeRow({
|
|
1824
|
-
...
|
|
2650
|
+
...source,
|
|
1825
2651
|
...shadow ? { mode: "shadow" } : {},
|
|
1826
2652
|
...ledger === void 0 ? {} : { ledger },
|
|
1827
2653
|
...approver === void 0 ? {} : { approver }
|
|
@@ -1834,7 +2660,7 @@ async function init(argv) {
|
|
|
1834
2660
|
}
|
|
1835
2661
|
console.log(`profile ${profile.dir}`);
|
|
1836
2662
|
console.log(`overlay ${plan.patchPath}`);
|
|
1837
|
-
console.log(`pack ${
|
|
2663
|
+
console.log(`pack ${packLine}`);
|
|
1838
2664
|
if (shadow) console.log("mode shadow · every verdict sealed, every call proceeds");
|
|
1839
2665
|
if (ledger !== void 0) console.log(`ledger ${ledger}`);
|
|
1840
2666
|
if (plan.findings.length > 0) {
|
|
@@ -1861,8 +2687,55 @@ async function init(argv) {
|
|
|
1861
2687
|
return fail(error instanceof Error ? error.message : String(error));
|
|
1862
2688
|
}
|
|
1863
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
|
+
}
|
|
1864
2737
|
async function checkpoint(path, keyPath, out) {
|
|
1865
|
-
const { makeCheckpoint } = await import("./checkpoint-
|
|
2738
|
+
const { makeCheckpoint } = await import("./checkpoint-C_q_Rpa5.js");
|
|
1866
2739
|
let ledger;
|
|
1867
2740
|
try {
|
|
1868
2741
|
ledger = Ledger.open(path);
|
|
@@ -1920,8 +2793,8 @@ async function main(argv) {
|
|
|
1920
2793
|
switch (command) {
|
|
1921
2794
|
case "check": {
|
|
1922
2795
|
const path = rest[0];
|
|
1923
|
-
if (path === void 0) fail("check needs a pack file");
|
|
1924
|
-
return check(path);
|
|
2796
|
+
if (path === void 0) fail("check needs a pack file or directory");
|
|
2797
|
+
return check(path, rest);
|
|
1925
2798
|
}
|
|
1926
2799
|
case "explain": {
|
|
1927
2800
|
const [path, tool, ...pairs] = rest;
|
|
@@ -1964,7 +2837,12 @@ async function main(argv) {
|
|
|
1964
2837
|
case "verify": {
|
|
1965
2838
|
const path = rest[0];
|
|
1966
2839
|
if (path === void 0) fail("verify needs a ledger file");
|
|
1967
|
-
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);
|
|
1968
2846
|
}
|
|
1969
2847
|
case "checkpoint": {
|
|
1970
2848
|
const path = rest[0];
|