prae-cli 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bin.js +2 -6
- package/lib/{checkpoint-BWSuhZ5h.js → checkpoint-BqJ8xwky.js} +1 -1
- package/lib/cli-BcAJlB9B.js +8 -0
- package/lib/{cli-7yLG89gV.js → cli-DPldZ2ND.js} +1122 -511
- package/lib/docs/pilot-claude-code.md +191 -0
- package/lib/{evaluate-wMPkjcIC.js → evaluate-DhoGQw0Z.js} +32 -1
- package/lib/frameworks-BgZHGKiH.js +118 -0
- package/lib/hook-Gll2spTP.js +695 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +6 -4
- package/lib/init-DRd-IRR6.js +266 -0
- package/lib/{instructions-Bg5puOen.js → instructions-DihPiiC2.js} +2 -1
- package/lib/{mcp-doNYU-1C.js → mcp-D521EoP6.js} +2 -2
- package/lib/pack-h5n43S0F.js +448 -0
- package/lib/playground.js +27 -3
- package/lib/{receipt-check-CD-W1wXs.js → receipt-check-CUuEjGHL.js} +1 -1
- package/lib/{src-D7g4lvaY.js → src-Cy-Acrgj.js} +111 -25
- package/lib/{src-CwBEFhww.js → src-DMoYsdfo.js} +1 -1
- package/lib/src-wG36kT6K.js +3 -0
- package/lib/{test-run-XNIRHKOu.js → test-run-C8dHjIE2.js} +1 -1
- package/lib/views-CUcixte0.js +1396 -0
- package/package.json +5 -4
- package/lib/src-d82lSKVQ.js +0 -3
- package/lib/views-CIg2ZNG0.js +0 -1401
- /package/lib/{ocsf-DhVGbBUw.js → ocsf-DF9cMiQJ.js} +0 -0
- /package/lib/{src-CBGZVvt7.js → src-BlmsTGdY.js} +0 -0
- /package/lib/{src-B25SHWcz.js → src-BuaOiL2v.js} +0 -0
- /package/lib/{src-d4c_n6wd.js → src-CDYyZyeU.js} +0 -0
|
@@ -1,438 +1,388 @@
|
|
|
1
|
-
import { t as evaluate } from "./evaluate-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { a as withinAny, r as hostOf, t as evaluate } from "./evaluate-DhoGQw0Z.js";
|
|
2
|
+
import { n as cite, t as FRAMEWORKS } from "./frameworks-BgZHGKiH.js";
|
|
3
|
+
import { t as parsePack } from "./pack-h5n43S0F.js";
|
|
4
|
+
import { a as verifyEntries, b as seal, g as GENESIS, h as verifySignature, l as verifyBytes, n as Ledger, r as readEntries, t as verifyGrowing, u as fingerprint, v as chain } from "./src-Cy-Acrgj.js";
|
|
5
|
+
import { a as renderPacks, c as renderNav, 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-CUcixte0.js";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
5
8
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
-
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
9
|
+
import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
7
11
|
import { homedir } from "node:os";
|
|
12
|
+
import { parse } from "yaml";
|
|
8
13
|
import { composePatchFile, preflightOps, revisionOf, takeSnapshot } from "dsh-overlay-check";
|
|
9
14
|
import { createServer } from "node:http";
|
|
10
15
|
|
|
11
|
-
//#region ../
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
check$1.keys(path, value, [
|
|
73
|
-
"name",
|
|
74
|
-
"token",
|
|
75
|
-
"anyToken",
|
|
76
|
-
"pipedTo",
|
|
77
|
-
"pipeline"
|
|
78
|
-
]);
|
|
79
|
-
if (value.name === void 0 && value.token === void 0 && value.anyToken === void 0 && value.pipedTo === void 0 && value.pipeline === void 0) check$1.fail(path, "expected `name`, `token`, `anyToken`, `pipedTo` or `pipeline`; a command matcher with none matches nothing");
|
|
80
|
-
const out = {};
|
|
81
|
-
if (value.token !== void 0) {
|
|
82
|
-
const tokens = check$1.names(`${path}.token`, value.token);
|
|
83
|
-
if (tokens !== void 0) out.token = tokens;
|
|
84
|
-
}
|
|
85
|
-
if (value.name !== void 0) {
|
|
86
|
-
const names = check$1.names(`${path}.name`, value.name);
|
|
87
|
-
if (names !== void 0) out.name = names;
|
|
88
|
-
}
|
|
89
|
-
if (value.anyToken !== void 0) {
|
|
90
|
-
const tokens = check$1.names(`${path}.anyToken`, value.anyToken);
|
|
91
|
-
if (tokens !== void 0) out.anyToken = tokens;
|
|
92
|
-
}
|
|
93
|
-
if (value.pipedTo !== void 0) {
|
|
94
|
-
const pipes = check$1.names(`${path}.pipedTo`, value.pipedTo);
|
|
95
|
-
if (pipes !== void 0) out.pipedTo = pipes;
|
|
96
|
-
}
|
|
97
|
-
if (value.pipeline !== void 0) {
|
|
98
|
-
const shapes = check$1.names(`${path}.pipeline`, value.pipeline);
|
|
99
|
-
if (shapes !== void 0) out.pipeline = shapes;
|
|
100
|
-
}
|
|
101
|
-
return out;
|
|
102
|
-
}
|
|
103
|
-
function readPathMatch(check$1, path, value) {
|
|
104
|
-
if (!isObject(value)) {
|
|
105
|
-
check$1.fail(path, "expected an object");
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
check$1.keys(path, value, [
|
|
109
|
-
"arg",
|
|
110
|
-
"outside",
|
|
111
|
-
"inside"
|
|
112
|
-
]);
|
|
113
|
-
if (value.outside === void 0 && value.inside === void 0) check$1.fail(path, "expected `inside` or `outside`; a path matcher with neither matches nothing");
|
|
114
|
-
const out = {};
|
|
115
|
-
if (value.arg !== void 0) {
|
|
116
|
-
const arg = check$1.text(`${path}.arg`, value.arg);
|
|
117
|
-
if (arg !== void 0) out.arg = arg;
|
|
118
|
-
}
|
|
119
|
-
if (value.outside !== void 0) {
|
|
120
|
-
const globs = check$1.globs(`${path}.outside`, value.outside);
|
|
121
|
-
if (globs !== void 0) out.outside = globs;
|
|
122
|
-
}
|
|
123
|
-
if (value.inside !== void 0) {
|
|
124
|
-
const globs = check$1.globs(`${path}.inside`, value.inside);
|
|
125
|
-
if (globs !== void 0) out.inside = globs;
|
|
126
|
-
}
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
function readHostMatch(check$1, path, value) {
|
|
130
|
-
if (!isObject(value)) {
|
|
131
|
-
check$1.fail(path, "expected an object");
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
check$1.keys(path, value, [
|
|
135
|
-
"arg",
|
|
136
|
-
"in",
|
|
137
|
-
"notIn"
|
|
138
|
-
]);
|
|
139
|
-
if (value.in === void 0 && value.notIn === void 0) check$1.fail(path, "expected `in` or `notIn`; a host matcher with neither matches nothing");
|
|
140
|
-
const out = {};
|
|
141
|
-
if (value.arg !== void 0) {
|
|
142
|
-
const arg = check$1.text(`${path}.arg`, value.arg);
|
|
143
|
-
if (arg !== void 0) out.arg = arg;
|
|
144
|
-
}
|
|
145
|
-
if (value.in !== void 0) {
|
|
146
|
-
const list = check$1.globs(`${path}.in`, value.in);
|
|
147
|
-
if (list !== void 0) out.in = list;
|
|
16
|
+
//#region ../dsh/src/chain.ts
|
|
17
|
+
const STAGES = [
|
|
18
|
+
{
|
|
19
|
+
id: "untrusted-input",
|
|
20
|
+
step: 1,
|
|
21
|
+
name: "UNTRUSTED INPUT",
|
|
22
|
+
severity: "INFO",
|
|
23
|
+
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.",
|
|
24
|
+
rule: "fs:read of a path under the untrusted globs (default **/tickets/**), or a row whose screening outcome is steer or hold.",
|
|
25
|
+
owasp: ["ASI01"],
|
|
26
|
+
atlas: ["AML.T0051.001"],
|
|
27
|
+
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."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "credential-access",
|
|
31
|
+
step: 2,
|
|
32
|
+
name: "CREDENTIAL ACCESS",
|
|
33
|
+
severity: "HIGH",
|
|
34
|
+
definition: "The agent tried to reach a credential: a file that is one by its path, or content the inspector found holding one.",
|
|
35
|
+
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, …).",
|
|
36
|
+
owasp: ["ASI03"],
|
|
37
|
+
atlas: [
|
|
38
|
+
"AML.TA0013",
|
|
39
|
+
"AML.T0055",
|
|
40
|
+
"AML.T0098"
|
|
41
|
+
],
|
|
42
|
+
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)."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "exfiltration",
|
|
46
|
+
step: 3,
|
|
47
|
+
name: "EXFILTRATION ATTEMPT",
|
|
48
|
+
severity: "CRITICAL",
|
|
49
|
+
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.",
|
|
50
|
+
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.",
|
|
51
|
+
owasp: ["ASI02"],
|
|
52
|
+
atlas: ["AML.TA0010", "AML.T0086"],
|
|
53
|
+
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."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "token-abuse",
|
|
57
|
+
step: 4,
|
|
58
|
+
name: "TOKEN ABUSE",
|
|
59
|
+
severity: "CRITICAL",
|
|
60
|
+
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.",
|
|
61
|
+
rule: "Stage 3, and the URL path has a segment token, oauth, oauth2 or sts.",
|
|
62
|
+
owasp: ["ASI03"],
|
|
63
|
+
atlas: ["AML.TA0013", "AML.TA0010"],
|
|
64
|
+
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."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "runaway",
|
|
68
|
+
step: 5,
|
|
69
|
+
name: "RUNAWAY BEHAVIOUR",
|
|
70
|
+
severity: "HIGH",
|
|
71
|
+
definition: "The agent kept calling past a budget the pack sets for the session.",
|
|
72
|
+
rule: "The deciding or a matched rule has a count matcher.",
|
|
73
|
+
owasp: ["ASI08"],
|
|
74
|
+
atlas: ["AML.T0034.002"],
|
|
75
|
+
alignment: "ATLAS Agentic Resource Consumption. OWASP ASI08 (Cascading Failures) is the nearest Agentic entry, not an equivalent."
|
|
148
76
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
77
|
+
];
|
|
78
|
+
/** Paths whose content the classifier treats as from outside the boundary. */
|
|
79
|
+
const UNTRUSTED_GLOBS = ["**/tickets/**", "./tickets/**"];
|
|
80
|
+
/** Paths that are credentials by location, whatever they contain. */
|
|
81
|
+
const CREDENTIAL_GLOBS = [
|
|
82
|
+
"/etc/shadow",
|
|
83
|
+
"/etc/gshadow",
|
|
84
|
+
"/etc/sudoers",
|
|
85
|
+
"**/.ssh/**",
|
|
86
|
+
"**/.aws/**",
|
|
87
|
+
"**/.gnupg/**",
|
|
88
|
+
"**/*.pem",
|
|
89
|
+
"**/*.key",
|
|
90
|
+
"**/.env",
|
|
91
|
+
"**/.env.*",
|
|
92
|
+
"**/credentials/**",
|
|
93
|
+
"**/.netrc"
|
|
94
|
+
];
|
|
95
|
+
const NETWORK_CLIENTS = new Set([
|
|
96
|
+
"curl",
|
|
97
|
+
"wget",
|
|
98
|
+
"nc",
|
|
99
|
+
"ncat",
|
|
100
|
+
"netcat",
|
|
101
|
+
"ssh",
|
|
102
|
+
"scp",
|
|
103
|
+
"sftp",
|
|
104
|
+
"ftp",
|
|
105
|
+
"telnet",
|
|
106
|
+
"openssl"
|
|
107
|
+
]);
|
|
108
|
+
const TOKEN_SEGMENT = new Set([
|
|
109
|
+
"token",
|
|
110
|
+
"oauth",
|
|
111
|
+
"oauth2",
|
|
112
|
+
"sts"
|
|
113
|
+
]);
|
|
114
|
+
function rulesOf(row$1, pack) {
|
|
115
|
+
if (pack === void 0) return [];
|
|
116
|
+
const ids = [row$1.rule, ...(row$1.trace ?? []).map((t) => t.rule)].filter((id) => id !== void 0);
|
|
117
|
+
return [...new Set(ids)].map((id) => pack.rules.find((r) => r.id === id)).filter((r) => r !== void 0);
|
|
118
|
+
}
|
|
119
|
+
function pathOf(url) {
|
|
120
|
+
try {
|
|
121
|
+
return new URL(url).pathname.toLowerCase().split("/").filter(Boolean);
|
|
122
|
+
} catch {
|
|
123
|
+
return [];
|
|
152
124
|
}
|
|
153
|
-
return out;
|
|
154
125
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
126
|
+
const stage = (id) => STAGES.find((s) => s.id === id);
|
|
127
|
+
/**
|
|
128
|
+
* The stage of one row, or undefined when the row is not an attack-chain
|
|
129
|
+
* step. Reads only the sealed row and the pack; the same inputs give the
|
|
130
|
+
* same answer every time. Checked in order: budget, credential, egress,
|
|
131
|
+
* untrusted input. The first that holds decides.
|
|
132
|
+
*/
|
|
133
|
+
function classify(row$1, pack) {
|
|
134
|
+
if (row$1.tool.startsWith("prae:")) return void 0;
|
|
135
|
+
const rules = rulesOf(row$1, pack);
|
|
136
|
+
const target = row$1.subject ?? "";
|
|
137
|
+
const budget = rules.find((r) => r.when.count !== void 0);
|
|
138
|
+
if (budget !== void 0) return {
|
|
139
|
+
stage: stage("runaway"),
|
|
140
|
+
basis: `${budget.id} is a session budget (count above ${budget.when.count?.above})`
|
|
141
|
+
};
|
|
142
|
+
const secrets = rules.find((r) => r.when.secrets !== void 0);
|
|
143
|
+
if (secrets !== void 0 || (row$1.secrets?.length ?? 0) > 0) return {
|
|
144
|
+
stage: stage("credential-access"),
|
|
145
|
+
basis: secrets !== void 0 ? `${secrets.id} inspects content for credentials, and matched` : `content inspection found ${row$1.secrets?.length} credential finding(s)`
|
|
146
|
+
};
|
|
147
|
+
if (target !== "" && (row$1.capability === "fs:read" || row$1.capability === "fs:write" || row$1.capability === "proc:spawn") && withinAny(target, CREDENTIAL_GLOBS)) return {
|
|
148
|
+
stage: stage("credential-access"),
|
|
149
|
+
basis: `the target ${target} is a credential by its path`
|
|
150
|
+
};
|
|
151
|
+
const host = rules.find((r) => r.when.host !== void 0);
|
|
152
|
+
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;
|
|
153
|
+
const egress = row$1.capability === "net:fetch" ? host : shell;
|
|
154
|
+
if (egress !== void 0) {
|
|
155
|
+
const token = row$1.capability === "net:fetch" ? pathOf(target).find((s) => TOKEN_SEGMENT.has(s)) : void 0;
|
|
156
|
+
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}`;
|
|
157
|
+
return token !== void 0 ? {
|
|
158
|
+
stage: stage("token-abuse"),
|
|
159
|
+
basis: `${where} (${egress.id}), and the path names a token endpoint (/${token})`
|
|
160
|
+
} : {
|
|
161
|
+
stage: stage("exfiltration"),
|
|
162
|
+
basis: `${where} (${egress.id})`
|
|
163
|
+
};
|
|
159
164
|
}
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const name = check$1.text(`${path}.name`, value.name);
|
|
169
|
-
if (name === void 0) return void 0;
|
|
170
|
-
const out = { name };
|
|
171
|
-
if (value.above !== void 0) {
|
|
172
|
-
const n = check$1.number(`${path}.above`, value.above);
|
|
173
|
-
if (n !== void 0) out.above = n;
|
|
174
|
-
}
|
|
175
|
-
if (value.below !== void 0) {
|
|
176
|
-
const n = check$1.number(`${path}.below`, value.below);
|
|
177
|
-
if (n !== void 0) out.below = n;
|
|
178
|
-
}
|
|
179
|
-
if (value.equals !== void 0) out.equals = value.equals;
|
|
180
|
-
if (value.oneOf !== void 0) if (!Array.isArray(value.oneOf)) check$1.fail(`${path}.oneOf`, "expected an array");
|
|
181
|
-
else out.oneOf = value.oneOf;
|
|
182
|
-
if (value.present !== void 0) if (typeof value.present !== "boolean") check$1.fail(`${path}.present`, "expected a boolean");
|
|
183
|
-
else out.present = value.present;
|
|
184
|
-
return out;
|
|
165
|
+
if (row$1.screening !== void 0 && (row$1.screening.outcome === "steer" || row$1.screening.outcome === "hold")) return {
|
|
166
|
+
stage: stage("untrusted-input"),
|
|
167
|
+
basis: `screening found instructions in the content (${row$1.screening.outcome})`
|
|
168
|
+
};
|
|
169
|
+
if (row$1.capability === "fs:read" && target !== "" && withinAny(target, UNTRUSTED_GLOBS)) return {
|
|
170
|
+
stage: stage("untrusted-input"),
|
|
171
|
+
basis: `${target} is customer-supplied content`
|
|
172
|
+
};
|
|
185
173
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const n = check$1.number(`${path}.atLeast`, value.atLeast);
|
|
208
|
-
if (n !== void 0) if (!Number.isInteger(n) || n < 1) check$1.fail(`${path}.atLeast`, "expected an integer of at least 1");
|
|
209
|
-
else out.atLeast = n;
|
|
210
|
-
}
|
|
211
|
-
return out;
|
|
174
|
+
const EXECUTION_LABEL = {
|
|
175
|
+
"blocked-before-execution": "BLOCKED BEFORE EXECUTION",
|
|
176
|
+
"held-before-execution": "HELD BEFORE EXECUTION",
|
|
177
|
+
"executed-output-withheld": "EXECUTED · DETECTED AFTER EXECUTION",
|
|
178
|
+
"executed-shadow": "EXECUTED · SHADOW (WOULD HAVE BEEN REFUSED)",
|
|
179
|
+
executed: "EXECUTED",
|
|
180
|
+
permitted: "PERMITTED"
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Whether the call ran, from the row alone. A refusal before execution and
|
|
184
|
+
* a result withheld after it are different claims, and the row says which:
|
|
185
|
+
* `phase: output` is the second.
|
|
186
|
+
*/
|
|
187
|
+
function executionOf(row$1) {
|
|
188
|
+
if (row$1.tool.startsWith("prae:")) return void 0;
|
|
189
|
+
const refused = row$1.verdict === "deny" || row$1.verdict === "correct";
|
|
190
|
+
if (row$1.shadow === true && (refused || row$1.verdict === "ask")) return "executed-shadow";
|
|
191
|
+
if (row$1.phase === "output") return refused ? "executed-output-withheld" : "executed";
|
|
192
|
+
if (refused) return "blocked-before-execution";
|
|
193
|
+
if (row$1.verdict === "ask") return "held-before-execution";
|
|
194
|
+
return row$1.executed === true ? "executed" : "permitted";
|
|
212
195
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (match !== void 0) out.path = match;
|
|
247
|
-
}
|
|
248
|
-
if (value.host !== void 0) {
|
|
249
|
-
const match = readHostMatch(check$1, `${path}.host`, value.host);
|
|
250
|
-
if (match !== void 0) out.host = match;
|
|
251
|
-
}
|
|
252
|
-
if (value.arg !== void 0) if (!Array.isArray(value.arg)) check$1.fail(`${path}.arg`, "expected an array of argument matchers");
|
|
253
|
-
else {
|
|
254
|
-
const list = [];
|
|
255
|
-
value.arg.forEach((entry, index) => {
|
|
256
|
-
const one = readArgMatch(check$1, `${path}.arg[${index}]`, entry);
|
|
257
|
-
if (one !== void 0) list.push(one);
|
|
196
|
+
/**
|
|
197
|
+
* The Attack Chain Integrity Matrix: every classified row, in ledger order,
|
|
198
|
+
* from attempted action to the seal that proves it. The Guardian comes from
|
|
199
|
+
* the catalogue the caller passes; the rest is the row's own.
|
|
200
|
+
*/
|
|
201
|
+
function integrityMatrix(entries, pack, guardianOf = () => void 0) {
|
|
202
|
+
const out = [];
|
|
203
|
+
for (const row$1 of entries) {
|
|
204
|
+
const c = classify(row$1, pack);
|
|
205
|
+
const execution = executionOf(row$1);
|
|
206
|
+
if (c === void 0 || execution === void 0) continue;
|
|
207
|
+
const rule = row$1.rule ?? row$1.trace?.[0]?.rule;
|
|
208
|
+
const mismatch = entries.find((e) => e.rule === "skill.capability_mismatch" && e.about === row$1.seq)?.seq;
|
|
209
|
+
out.push({
|
|
210
|
+
seq: row$1.seq,
|
|
211
|
+
at: row$1.at,
|
|
212
|
+
action: row$1.tool,
|
|
213
|
+
...row$1.subject === void 0 ? {} : { target: row$1.subject },
|
|
214
|
+
...row$1.capability === void 0 ? {} : { capability: row$1.capability },
|
|
215
|
+
...guardianOf(rule) === void 0 ? {} : { guardian: guardianOf(rule) },
|
|
216
|
+
...rule === void 0 ? {} : {
|
|
217
|
+
rule,
|
|
218
|
+
ruleRole: row$1.rule === void 0 ? "recorded" : "decided"
|
|
219
|
+
},
|
|
220
|
+
decision: row$1.verdict.toUpperCase(),
|
|
221
|
+
...row$1.reason === void 0 ? {} : { reason: row$1.reason },
|
|
222
|
+
execution: EXECUTION_LABEL[execution],
|
|
223
|
+
stage: `${c.stage.step}/5 ${c.stage.name}`,
|
|
224
|
+
basis: c.basis,
|
|
225
|
+
...mismatch === void 0 ? {} : { mismatch },
|
|
226
|
+
hash: row$1.hash,
|
|
227
|
+
prev: row$1.prev,
|
|
228
|
+
signed: row$1.sig !== void 0
|
|
258
229
|
});
|
|
259
|
-
out.arg = list;
|
|
260
|
-
}
|
|
261
|
-
if (value.count !== void 0) if (!isObject(value.count)) check$1.fail(`${path}.count`, "expected an object");
|
|
262
|
-
else {
|
|
263
|
-
check$1.keys(`${path}.count`, value.count, ["above"]);
|
|
264
|
-
const above = check$1.number(`${path}.count.above`, value.count.above);
|
|
265
|
-
if (above !== void 0) out.count = { above };
|
|
266
|
-
}
|
|
267
|
-
if (value.secrets !== void 0) {
|
|
268
|
-
const match = readSecretsMatch(check$1, `${path}.secrets`, value.secrets);
|
|
269
|
-
if (match !== void 0) out.secrets = match;
|
|
270
230
|
}
|
|
271
231
|
return out;
|
|
272
232
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
233
|
+
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region ../dsh/src/packset.ts
|
|
236
|
+
const PACK_FILE = /\.prae\.(json|ya?ml)$/;
|
|
237
|
+
/** Read one pack file: its parsed pack, or why it cannot be one. */
|
|
238
|
+
function readPackFile(file, policyKey, origin = "workspace") {
|
|
239
|
+
let bytes;
|
|
240
|
+
try {
|
|
241
|
+
bytes = readFileSync(file);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
return {
|
|
244
|
+
file,
|
|
245
|
+
reason: `unreadable: ${e instanceof Error ? e.message : String(e)}`
|
|
246
|
+
};
|
|
277
247
|
}
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
248
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
249
|
+
if (policyKey !== void 0) {
|
|
250
|
+
let signature = "";
|
|
251
|
+
try {
|
|
252
|
+
signature = readFileSync(`${file}.sig`, "utf8");
|
|
253
|
+
} catch {}
|
|
254
|
+
if (signature === "") return {
|
|
255
|
+
file,
|
|
256
|
+
reason: `unsigned: no ${file.split("/").at(-1)}.sig beside it, and a policy key is pinned`
|
|
257
|
+
};
|
|
258
|
+
if (!verifyBytes(bytes, signature, policyKey)) return {
|
|
259
|
+
file,
|
|
260
|
+
reason: `not signed by the policy key ${fingerprint(policyKey)} (edited after signing, or signed by another key)`
|
|
261
|
+
};
|
|
282
262
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
] : ["kind", "because"]);
|
|
288
|
-
const because = check$1.text(`${path}.because`, value.because);
|
|
289
|
-
if (because === void 0) return void 0;
|
|
290
|
-
if (kind === "correct") {
|
|
291
|
-
if (!isObject(value.set) || Object.keys(value.set).length === 0) {
|
|
292
|
-
check$1.fail(`${path}.set`, "expected a non-empty object of arguments to set");
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
263
|
+
let json = bytes.toString("utf8");
|
|
264
|
+
if (/\.ya?ml$/.test(file)) try {
|
|
265
|
+
json = JSON.stringify(parse(json));
|
|
266
|
+
} catch (e) {
|
|
295
267
|
return {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
because
|
|
268
|
+
file,
|
|
269
|
+
reason: `invalid YAML: ${e instanceof Error ? e.message : String(e)}`
|
|
299
270
|
};
|
|
300
271
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
272
|
+
const parsed$1 = parsePack(json);
|
|
273
|
+
if (!parsed$1.ok) return {
|
|
274
|
+
file,
|
|
275
|
+
reason: parsed$1.errors.map((e) => `${e.path || "<root>"}: ${e.message}`).join("; ")
|
|
304
276
|
};
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
check$1.keys(path, value, [
|
|
312
|
-
"id",
|
|
313
|
-
"title",
|
|
314
|
-
"when",
|
|
315
|
-
"effect",
|
|
316
|
-
"frameworks"
|
|
317
|
-
]);
|
|
318
|
-
const id = check$1.text(`${path}.id`, value.id);
|
|
319
|
-
const effect = readEffect(check$1, `${path}.effect`, value.effect);
|
|
320
|
-
if (value.when === void 0) check$1.fail(`${path}.when`, "expected an object");
|
|
321
|
-
const when = readMatch(check$1, `${path}.when`, value.when ?? {});
|
|
322
|
-
if (id === void 0 || effect === void 0) return void 0;
|
|
323
|
-
const rule = {
|
|
324
|
-
id,
|
|
325
|
-
when,
|
|
326
|
-
effect
|
|
277
|
+
return {
|
|
278
|
+
file,
|
|
279
|
+
pack: parsed$1.pack,
|
|
280
|
+
origin,
|
|
281
|
+
sha256,
|
|
282
|
+
...policyKey === void 0 ? {} : { signedBy: fingerprint(policyKey) }
|
|
327
283
|
};
|
|
328
|
-
if (value.title !== void 0) {
|
|
329
|
-
const title = check$1.text(`${path}.title`, value.title);
|
|
330
|
-
if (title !== void 0) rule.title = title;
|
|
331
|
-
}
|
|
332
|
-
if (value.frameworks !== void 0) {
|
|
333
|
-
const frameworks = readFrameworks(check$1, `${path}.frameworks`, value.frameworks);
|
|
334
|
-
if (frameworks !== void 0) rule.frameworks = frameworks;
|
|
335
|
-
}
|
|
336
|
-
return rule;
|
|
337
284
|
}
|
|
338
|
-
/**
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
285
|
+
/** Load, validate and merge every pack in a directory. Never throws. */
|
|
286
|
+
function loadPackDir(dir, policyKey, accept, builtin) {
|
|
287
|
+
const packs = [];
|
|
288
|
+
const rejected = [];
|
|
289
|
+
const ids = /* @__PURE__ */ new Map();
|
|
290
|
+
let baselineFrom;
|
|
291
|
+
let names = [];
|
|
292
|
+
try {
|
|
293
|
+
names = readdirSync(resolve(dir)).filter((n) => PACK_FILE.test(n)).sort();
|
|
294
|
+
} catch (e) {
|
|
295
|
+
rejected.push({
|
|
296
|
+
file: dir,
|
|
297
|
+
reason: `no such directory: ${e instanceof Error ? e.message : String(e)}`
|
|
298
|
+
});
|
|
348
299
|
}
|
|
349
|
-
const
|
|
350
|
-
for (const
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
300
|
+
const candidates = [...builtin === void 0 ? [] : [builtin], ...names.map((name) => readPackFile(join(resolve(dir), name), policyKey))];
|
|
301
|
+
for (const read of candidates) {
|
|
302
|
+
const file = read.file;
|
|
303
|
+
const name = file.split("/").at(-1) ?? file;
|
|
304
|
+
if (!("pack" in read)) {
|
|
305
|
+
rejected.push(read);
|
|
354
306
|
continue;
|
|
355
307
|
}
|
|
356
|
-
|
|
357
|
-
|
|
308
|
+
const refused = accept?.(read);
|
|
309
|
+
if (refused !== void 0) {
|
|
310
|
+
rejected.push({
|
|
311
|
+
file,
|
|
312
|
+
reason: refused
|
|
313
|
+
});
|
|
358
314
|
continue;
|
|
359
315
|
}
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
316
|
+
const foreign = read.origin === "workspace" ? read.pack.rules.find((r) => !r.id.startsWith("ORG-")) : void 0;
|
|
317
|
+
if (foreign !== void 0) {
|
|
318
|
+
rejected.push({
|
|
319
|
+
file,
|
|
320
|
+
reason: `rule id ${foreign.id} must start with ORG-; workspace rules are the organisation's own, and other ids belong to built-in packs`
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const clash = read.pack.rules.find((r) => ids.has(r.id));
|
|
325
|
+
if (clash !== void 0) {
|
|
326
|
+
rejected.push({
|
|
327
|
+
file,
|
|
328
|
+
reason: `rule id ${clash.id} is already defined by ${ids.get(clash.id)}; rule ids must be unique across the workspace`
|
|
329
|
+
});
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (read.pack.baseline !== void 0 && baselineFrom !== void 0) {
|
|
333
|
+
rejected.push({
|
|
334
|
+
file,
|
|
335
|
+
reason: `declares a baseline, and ${baselineFrom} already does; only one pack in a workspace may, so a grant is never widened`
|
|
336
|
+
});
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (read.pack.baseline !== void 0) baselineFrom = name;
|
|
340
|
+
for (const r of read.pack.rules) ids.set(r.id, name);
|
|
341
|
+
packs.push(read);
|
|
342
|
+
}
|
|
343
|
+
return merge(packs, rejected);
|
|
344
|
+
}
|
|
345
|
+
/** The packs as one policy, each rule remembering where it came from. */
|
|
346
|
+
function merge(packs, rejected) {
|
|
347
|
+
const origins = /* @__PURE__ */ new Map();
|
|
348
|
+
for (const p of packs) for (const r of p.pack.rules) origins.set(r.id, {
|
|
349
|
+
name: p.pack.pack,
|
|
350
|
+
version: p.pack.version,
|
|
351
|
+
sha256: p.sha256,
|
|
352
|
+
origin: p.origin
|
|
353
|
+
});
|
|
354
|
+
if (packs.length === 0) return {
|
|
355
|
+
packs,
|
|
356
|
+
rejected,
|
|
357
|
+
origins,
|
|
358
|
+
effective: {
|
|
359
|
+
prae: "0.1",
|
|
360
|
+
pack: "no-valid-pack",
|
|
361
|
+
version: "0",
|
|
362
|
+
rules: [{
|
|
363
|
+
id: "PRAE-NO-PACK",
|
|
364
|
+
when: {},
|
|
365
|
+
effect: {
|
|
366
|
+
kind: "deny",
|
|
367
|
+
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(" · ")})`
|
|
368
|
+
}
|
|
377
369
|
}]
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
const check$1 = new Check();
|
|
381
|
-
if (!isObject(raw)) return {
|
|
382
|
-
ok: false,
|
|
383
|
-
errors: [{
|
|
384
|
-
path: "",
|
|
385
|
-
message: "expected an object"
|
|
386
|
-
}]
|
|
387
|
-
};
|
|
388
|
-
check$1.keys("", raw, [
|
|
389
|
-
"prae",
|
|
390
|
-
"pack",
|
|
391
|
-
"version",
|
|
392
|
-
"baseline",
|
|
393
|
-
"rules"
|
|
394
|
-
]);
|
|
395
|
-
if (raw.prae !== "0.1") check$1.fail(".prae", "expected `\"0.1\"`; this is the only pack version this build reads");
|
|
396
|
-
const name = check$1.text(".pack", raw.pack);
|
|
397
|
-
const version = check$1.text(".version", raw.version);
|
|
398
|
-
let baseline;
|
|
399
|
-
if (raw.baseline !== void 0) if (!isObject(raw.baseline)) check$1.fail(".baseline", "expected an object");
|
|
400
|
-
else {
|
|
401
|
-
check$1.keys(".baseline", raw.baseline, ["capabilities"]);
|
|
402
|
-
const caps = check$1.globs(".baseline.capabilities", raw.baseline.capabilities);
|
|
403
|
-
if (caps !== void 0) baseline = { capabilities: caps };
|
|
404
|
-
}
|
|
405
|
-
const rules = [];
|
|
406
|
-
if (!Array.isArray(raw.rules)) check$1.fail(".rules", "expected an array");
|
|
407
|
-
else {
|
|
408
|
-
const seen = /* @__PURE__ */ new Set();
|
|
409
|
-
raw.rules.forEach((entry, index) => {
|
|
410
|
-
const rule = readRule(check$1, `rules[${index}]`, entry);
|
|
411
|
-
if (rule === void 0) return;
|
|
412
|
-
if (seen.has(rule.id)) {
|
|
413
|
-
check$1.fail(`rules[${index}].id`, `duplicate rule id \`${rule.id}\``);
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
seen.add(rule.id);
|
|
417
|
-
rules.push(rule);
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
if (check$1.errors.length > 0) return {
|
|
421
|
-
ok: false,
|
|
422
|
-
errors: check$1.errors
|
|
423
|
-
};
|
|
424
|
-
if (name === void 0 || version === void 0) return {
|
|
425
|
-
ok: false,
|
|
426
|
-
errors: check$1.errors
|
|
370
|
+
}
|
|
427
371
|
};
|
|
372
|
+
const baseline = packs.find((p) => p.pack.baseline !== void 0)?.pack.baseline;
|
|
373
|
+
const only = packs.length === 1 ? packs[0] : void 0;
|
|
374
|
+
const expires = packs.map((p) => p.pack.expires_at).filter((e) => e !== void 0).sort((a, b) => Date.parse(a) - Date.parse(b))[0];
|
|
428
375
|
return {
|
|
429
|
-
|
|
430
|
-
|
|
376
|
+
packs,
|
|
377
|
+
rejected,
|
|
378
|
+
origins,
|
|
379
|
+
effective: {
|
|
431
380
|
prae: "0.1",
|
|
432
|
-
pack:
|
|
433
|
-
version,
|
|
381
|
+
pack: only?.pack.pack ?? packs.map((p) => p.pack.pack).join("+"),
|
|
382
|
+
version: only?.pack.version ?? createHash("sha256").update(packs.map((p) => p.sha256).join("")).digest("hex").slice(0, 12),
|
|
434
383
|
...baseline === void 0 ? {} : { baseline },
|
|
435
|
-
|
|
384
|
+
...expires === void 0 ? {} : { expires_at: expires },
|
|
385
|
+
rules: packs.flatMap((p) => p.pack.rules)
|
|
436
386
|
}
|
|
437
387
|
};
|
|
438
388
|
}
|
|
@@ -457,7 +407,7 @@ function row(entry, pack) {
|
|
|
457
407
|
<div class="call">${esc(call)}</div>
|
|
458
408
|
<div class="actor">${esc(actor)}</div>
|
|
459
409
|
<div class="clause">${esc(entry.rule ?? "—")}${cited}</div>
|
|
460
|
-
<div class="verdict v-${esc(entry.verdict)}">${esc(VERDICT_LABEL$1[entry.verdict] ?? entry.verdict)}</div>
|
|
410
|
+
<div class="verdict v-${esc(entry.verdict)}">${esc(VERDICT_LABEL$1[entry.verdict] ?? entry.verdict)}${entry.shadow === true ? " · SHADOW" : ""}</div>
|
|
461
411
|
<div class="seal">${esc(seal(entry))}</div>
|
|
462
412
|
</div>`;
|
|
463
413
|
}
|
|
@@ -465,12 +415,14 @@ function renderLedgerView(entries, verification, options) {
|
|
|
465
415
|
const counts = {};
|
|
466
416
|
const packs = /* @__PURE__ */ new Set();
|
|
467
417
|
const sessions = /* @__PURE__ */ new Set();
|
|
418
|
+
let shadowed = 0;
|
|
468
419
|
for (const entry of entries) {
|
|
469
|
-
|
|
420
|
+
if (entry.shadow === true) shadowed += 1;
|
|
421
|
+
else counts[entry.verdict] = (counts[entry.verdict] ?? 0) + 1;
|
|
470
422
|
packs.add(`${entry.pack.name} ${entry.pack.version}`);
|
|
471
423
|
if (entry.session !== void 0) sessions.add(entry.session);
|
|
472
424
|
}
|
|
473
|
-
const held = entries.filter((e) => e.verdict === "ask");
|
|
425
|
+
const held = entries.filter((e) => e.verdict === "ask" && e.shadow !== true);
|
|
474
426
|
const pack = [...packs][0] ?? "—";
|
|
475
427
|
const now = Date.parse(options.generatedAt);
|
|
476
428
|
const chainOk = verification.ok;
|
|
@@ -480,6 +432,7 @@ function renderLedgerView(entries, verification, options) {
|
|
|
480
432
|
<div class="meta">${esc([
|
|
481
433
|
`${sessions.size || 1} session${sessions.size === 1 ? "" : "s"}`,
|
|
482
434
|
`${entries.length.toLocaleString("en-US")} decisions`,
|
|
435
|
+
...shadowed === 0 ? [] : [`${shadowed} in shadow mode (recorded, not enforced)`],
|
|
483
436
|
options.retention === void 0 ? "retention local" : `retention ${options.retention}`
|
|
484
437
|
].join(" · "))}</div>
|
|
485
438
|
</div>
|
|
@@ -763,6 +716,7 @@ function praeRow(options) {
|
|
|
763
716
|
if (options.ledger !== void 0) lines.push(` ledger: ${JSON.stringify(options.ledger)}`);
|
|
764
717
|
if (options.approver !== void 0) lines.push(` approver: ${JSON.stringify(options.approver)}`);
|
|
765
718
|
if (options.mode !== void 0) lines.push(` mode: ${options.mode}`);
|
|
719
|
+
if (options.packDir !== void 0) lines.push(` packDir: ${JSON.stringify(options.packDir)}`);
|
|
766
720
|
if (options.packFile !== void 0) lines.push(` packFile: ${JSON.stringify(options.packFile)}`);
|
|
767
721
|
else lines.push(" pack: |", indent(options.pack ?? "", 10));
|
|
768
722
|
return lines.join("\n");
|
|
@@ -814,16 +768,60 @@ function jsonOrNull(path) {
|
|
|
814
768
|
return null;
|
|
815
769
|
}
|
|
816
770
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
771
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
772
|
+
function parseLines(text, into) {
|
|
773
|
+
for (const line of text.split("\n")) {
|
|
821
774
|
if (line.trim() === "") continue;
|
|
822
775
|
try {
|
|
823
|
-
|
|
776
|
+
into.push(JSON.parse(line));
|
|
824
777
|
} catch {}
|
|
825
778
|
}
|
|
826
|
-
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* The ledger's rows. A ledger is append-only, so the parse is kept and only
|
|
782
|
+
* the bytes appended since are read: a long demo grows it to tens of
|
|
783
|
+
* thousands of rows, and every endpoint the console polls reads it — parsed
|
|
784
|
+
* whole each time, that kept the server too busy to answer anyone. A file
|
|
785
|
+
* that shrank or was replaced is read again from the start. A half-written
|
|
786
|
+
* last line is left unread until its newline arrives.
|
|
787
|
+
*/
|
|
788
|
+
function readLedger(path) {
|
|
789
|
+
if (!existsSync(path)) {
|
|
790
|
+
parsed.delete(path);
|
|
791
|
+
return [];
|
|
792
|
+
}
|
|
793
|
+
const stat$1 = statSync(path);
|
|
794
|
+
let entry = parsed.get(path);
|
|
795
|
+
if (entry === void 0 || entry.ino !== stat$1.ino || stat$1.size < entry.size) {
|
|
796
|
+
entry = {
|
|
797
|
+
ino: stat$1.ino,
|
|
798
|
+
size: 0,
|
|
799
|
+
mtimeMs: 0,
|
|
800
|
+
offset: 0,
|
|
801
|
+
rows: []
|
|
802
|
+
};
|
|
803
|
+
parsed.set(path, entry);
|
|
804
|
+
}
|
|
805
|
+
if (stat$1.size !== entry.size || stat$1.mtimeMs !== entry.mtimeMs) {
|
|
806
|
+
const length = stat$1.size - entry.offset;
|
|
807
|
+
if (length > 0) {
|
|
808
|
+
const buf = Buffer.alloc(length);
|
|
809
|
+
const fd = openSync(path, "r");
|
|
810
|
+
try {
|
|
811
|
+
readSync(fd, buf, 0, length, entry.offset);
|
|
812
|
+
} finally {
|
|
813
|
+
closeSync(fd);
|
|
814
|
+
}
|
|
815
|
+
const end = buf.lastIndexOf(10);
|
|
816
|
+
if (end >= 0) {
|
|
817
|
+
parseLines(buf.subarray(0, end).toString("utf8"), entry.rows);
|
|
818
|
+
entry.offset += end + 1;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
entry.size = stat$1.size;
|
|
822
|
+
entry.mtimeMs = stat$1.mtimeMs;
|
|
823
|
+
}
|
|
824
|
+
return entry.rows.slice();
|
|
827
825
|
}
|
|
828
826
|
/** The defaults the demo writes; a caller with other names overrides them. */
|
|
829
827
|
const DEFAULT_FILES = {
|
|
@@ -832,6 +830,7 @@ const DEFAULT_FILES = {
|
|
|
832
830
|
report: "security-report.json",
|
|
833
831
|
guardians: "guardians.json",
|
|
834
832
|
pack: "papaya.prae.json",
|
|
833
|
+
packs: ".prae/packs",
|
|
835
834
|
publicKey: "prae-signing.pub.pem",
|
|
836
835
|
redteam: "../redteam/results"
|
|
837
836
|
};
|
|
@@ -841,7 +840,8 @@ function readRecord(dir, files) {
|
|
|
841
840
|
discovery: jsonOrNull(resolve(dir, files.discovery)),
|
|
842
841
|
report: jsonOrNull(resolve(dir, files.report)),
|
|
843
842
|
guardians: jsonOrNull(resolve(dir, files.guardians)),
|
|
844
|
-
pack: jsonOrNull(resolve(dir, files.pack))
|
|
843
|
+
pack: jsonOrNull(resolve(dir, files.pack)),
|
|
844
|
+
crownJewel: jsonOrNull(resolve(dir, "crown-jewel.json"))
|
|
845
845
|
};
|
|
846
846
|
}
|
|
847
847
|
/** The red-team campaign and its runs, as the runner wrote them; empty when none. */
|
|
@@ -849,12 +849,362 @@ function readRedteam(dir, files) {
|
|
|
849
849
|
const root = resolve(dir, files.redteam);
|
|
850
850
|
if (!existsSync(root)) return {
|
|
851
851
|
campaign: null,
|
|
852
|
-
runs: []
|
|
852
|
+
runs: [],
|
|
853
|
+
steering: [],
|
|
854
|
+
history: []
|
|
853
855
|
};
|
|
854
856
|
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);
|
|
857
|
+
const steeringDir = resolve(root, "steering");
|
|
858
|
+
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) : [];
|
|
859
|
+
const historyDir = resolve(steeringDir, "history");
|
|
860
|
+
const history = existsSync(historyDir) ? readdirSync(historyDir).filter((n) => n.endsWith(".json")).sort().reverse().map((n) => jsonOrNull(resolve(historyDir, n))).filter((r) => r !== null) : [];
|
|
855
861
|
return {
|
|
856
862
|
campaign: jsonOrNull(resolve(root, "campaign.json")),
|
|
857
|
-
runs
|
|
863
|
+
runs,
|
|
864
|
+
steering,
|
|
865
|
+
history
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region ../controls/src/scope.ts
|
|
871
|
+
const ROW_LIMIT = 50;
|
|
872
|
+
/** A framework's edition, read from its versioned id (`…-2026`). */
|
|
873
|
+
const versionOf = (id) => /(\d{4})$/.exec(id)?.[1];
|
|
874
|
+
/** Where a built-in control belongs, and its code within it. */
|
|
875
|
+
function placeOf(control) {
|
|
876
|
+
const found = Object.entries(FRAMEWORKS).find(([, f]) => control.id.startsWith(`${f.short} · `));
|
|
877
|
+
if (found !== void 0) return {
|
|
878
|
+
framework: {
|
|
879
|
+
id: found[0],
|
|
880
|
+
name: found[1].name
|
|
881
|
+
},
|
|
882
|
+
id: control.id.slice(found[1].short.length + 3)
|
|
883
|
+
};
|
|
884
|
+
return {
|
|
885
|
+
framework: control.standard ?? {
|
|
886
|
+
id: "obligations",
|
|
887
|
+
name: "Obligations"
|
|
888
|
+
},
|
|
889
|
+
id: control.code ?? control.id.split(" · ")[1] ?? control.id
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
/** A control defined as data: evidence is the rows under its mapped rules. */
|
|
893
|
+
function fromData(def) {
|
|
894
|
+
const types = new Set(def.evidence_types ?? [
|
|
895
|
+
"allow",
|
|
896
|
+
"deny",
|
|
897
|
+
"ask"
|
|
898
|
+
]);
|
|
899
|
+
const trace = (data) => {
|
|
900
|
+
const ids = new Set(def.prae_controls);
|
|
901
|
+
return {
|
|
902
|
+
rules: [...def.prae_controls],
|
|
903
|
+
rows: data.entries.filter((e) => e.rule !== void 0 && ids.has(e.rule) && types.has(e.verdict))
|
|
904
|
+
};
|
|
905
|
+
};
|
|
906
|
+
return {
|
|
907
|
+
id: def.id,
|
|
908
|
+
...def.name === void 0 ? {} : { name: def.name },
|
|
909
|
+
asks: def.asks,
|
|
910
|
+
trace,
|
|
911
|
+
evidence: (data) => {
|
|
912
|
+
const inPack = new Set((data.pack?.rules ?? []).map((r) => r.id));
|
|
913
|
+
const present = def.prae_controls.filter((id) => inPack.has(id));
|
|
914
|
+
const rows = trace(data).rows.length;
|
|
915
|
+
if (rows > 0) return {
|
|
916
|
+
state: "covered",
|
|
917
|
+
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.`
|
|
918
|
+
};
|
|
919
|
+
if (present.length > 0) return {
|
|
920
|
+
state: "partial",
|
|
921
|
+
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.`
|
|
922
|
+
};
|
|
923
|
+
return {
|
|
924
|
+
state: "absent",
|
|
925
|
+
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.`
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/** The built-in catalog, plus a record's own frameworks when it has any. */
|
|
931
|
+
function catalogOf(file) {
|
|
932
|
+
const frameworks = /* @__PURE__ */ new Map();
|
|
933
|
+
const controls = /* @__PURE__ */ new Map();
|
|
934
|
+
const add = (framework, id, control) => {
|
|
935
|
+
const key = `${framework.id}:${id}`;
|
|
936
|
+
if (controls.has(key)) return;
|
|
937
|
+
controls.set(key, control);
|
|
938
|
+
let f = frameworks.get(framework.id);
|
|
939
|
+
if (f === void 0) {
|
|
940
|
+
f = {
|
|
941
|
+
...framework,
|
|
942
|
+
controls: []
|
|
943
|
+
};
|
|
944
|
+
frameworks.set(framework.id, f);
|
|
945
|
+
}
|
|
946
|
+
f.controls.push({
|
|
947
|
+
key,
|
|
948
|
+
id,
|
|
949
|
+
...control.name === void 0 ? {} : { name: control.name },
|
|
950
|
+
asks: control.asks
|
|
951
|
+
});
|
|
952
|
+
};
|
|
953
|
+
for (const control of CONTROLS) {
|
|
954
|
+
const place = placeOf(control);
|
|
955
|
+
const version = versionOf(place.framework.id);
|
|
956
|
+
add({
|
|
957
|
+
id: place.framework.id,
|
|
958
|
+
name: place.framework.name,
|
|
959
|
+
...version === void 0 ? {} : { version }
|
|
960
|
+
}, place.id, control);
|
|
961
|
+
}
|
|
962
|
+
for (const f of file?.frameworks ?? []) for (const c of f.controls) add({
|
|
963
|
+
id: f.id,
|
|
964
|
+
name: f.name,
|
|
965
|
+
...f.version === void 0 ? {} : { version: f.version },
|
|
966
|
+
...f.description === void 0 ? {} : { description: f.description },
|
|
967
|
+
custom: true
|
|
968
|
+
}, c.id, fromData(c));
|
|
969
|
+
return {
|
|
970
|
+
frameworks: [...frameworks.values()],
|
|
971
|
+
controls
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/** A catalog file, checked: what is wrong with it, or the file. */
|
|
975
|
+
function parseCatalog(raw) {
|
|
976
|
+
const text = (v) => typeof v === "string" && v.trim() !== "";
|
|
977
|
+
if (typeof raw !== "object" || raw === null) return "not an object";
|
|
978
|
+
const frameworks = raw.frameworks;
|
|
979
|
+
if (!Array.isArray(frameworks)) return "frameworks: expected an array";
|
|
980
|
+
for (const [i, f] of frameworks.entries()) {
|
|
981
|
+
if (!text(f?.id) || !/^[a-z0-9][a-z0-9-]*$/.test(f.id)) return `frameworks[${i}].id: lowercase letters, digits and dashes`;
|
|
982
|
+
if (!text(f.name)) return `frameworks[${i}].name: required`;
|
|
983
|
+
if (!Array.isArray(f.controls)) return `frameworks[${i}].controls: expected an array`;
|
|
984
|
+
for (const [j, c] of f.controls.entries()) {
|
|
985
|
+
const at = `frameworks[${i}].controls[${j}]`;
|
|
986
|
+
if (!text(c?.id)) return `${at}.id: required`;
|
|
987
|
+
if (!text(c.asks)) return `${at}.asks: required`;
|
|
988
|
+
if (!Array.isArray(c.prae_controls) || !c.prae_controls.every((r) => text(r))) return `${at}.prae_controls: expected an array of PRAE rule ids`;
|
|
989
|
+
if (c.evidence_types !== void 0 && (!Array.isArray(c.evidence_types) || !c.evidence_types.every((t) => [
|
|
990
|
+
"allow",
|
|
991
|
+
"deny",
|
|
992
|
+
"ask"
|
|
993
|
+
].includes(t)))) return `${at}.evidence_types: allow, deny or ask`;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return raw;
|
|
997
|
+
}
|
|
998
|
+
/** Everything built in, every agent and session, the whole record. */
|
|
999
|
+
function defaultScope(catalog) {
|
|
1000
|
+
const builtIn = catalog.frameworks.filter((f) => f.custom !== true);
|
|
1001
|
+
return {
|
|
1002
|
+
frameworks: builtIn.map((f) => f.id),
|
|
1003
|
+
controls: builtIn.flatMap((f) => f.controls.map((c) => c.key)),
|
|
1004
|
+
agents: "all",
|
|
1005
|
+
sessions: "all",
|
|
1006
|
+
period: { kind: "all" }
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/** A scope as saved or sent, checked: what is wrong with it, or the scope. */
|
|
1010
|
+
function parseScope(raw) {
|
|
1011
|
+
if (typeof raw !== "object" || raw === null) return "not an object";
|
|
1012
|
+
const r = raw;
|
|
1013
|
+
const strings = (v) => Array.isArray(v) && v.every((s) => typeof s === "string" && s.length <= 200);
|
|
1014
|
+
if (!strings(r.frameworks)) return "frameworks: expected an array of ids";
|
|
1015
|
+
if (!strings(r.controls)) return "controls: expected an array of keys";
|
|
1016
|
+
for (const d of ["agents", "sessions"]) if (r[d] !== "all" && !strings(r[d])) return `${d}: "all" or an array`;
|
|
1017
|
+
const p = r.period;
|
|
1018
|
+
const period = p?.kind === "all" ? { kind: "all" } : p?.kind === "days" && typeof p.days === "number" && Number.isInteger(p.days) && p.days > 0 && p.days <= 3650 ? {
|
|
1019
|
+
kind: "days",
|
|
1020
|
+
days: p.days
|
|
1021
|
+
} : 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) ? {
|
|
1022
|
+
kind: "custom",
|
|
1023
|
+
from: p.from,
|
|
1024
|
+
to: p.to
|
|
1025
|
+
} : "period: all, the last N days, or a custom from–to range";
|
|
1026
|
+
if (typeof period === "string") return period;
|
|
1027
|
+
return {
|
|
1028
|
+
frameworks: r.frameworks,
|
|
1029
|
+
controls: r.controls,
|
|
1030
|
+
agents: r.agents,
|
|
1031
|
+
sessions: r.sessions,
|
|
1032
|
+
period
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
/** Whether a row falls in the scope's period. */
|
|
1036
|
+
function inPeriod(at, period, now) {
|
|
1037
|
+
const t = Date.parse(at);
|
|
1038
|
+
if (period.kind === "all") return true;
|
|
1039
|
+
if (Number.isNaN(t)) return false;
|
|
1040
|
+
if (period.kind === "days") return t >= now - period.days * 864e5;
|
|
1041
|
+
return t >= Date.parse(period.from) && t < Date.parse(period.to) + 864e5;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Assess the scoped controls against the record. The rows are narrowed to
|
|
1045
|
+
* the scope's agents, sessions and period before any control reads them;
|
|
1046
|
+
* the chain's verification stays the whole record's, since integrity is a
|
|
1047
|
+
* property of the chain, not of a slice of it.
|
|
1048
|
+
*/
|
|
1049
|
+
function assess(data, scope, catalog, options) {
|
|
1050
|
+
const now = options.now ?? Date.now();
|
|
1051
|
+
const agentOf$1 = (e) => e.agent ?? options.agent;
|
|
1052
|
+
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));
|
|
1053
|
+
const scoped = {
|
|
1054
|
+
...data,
|
|
1055
|
+
entries: admitted
|
|
1056
|
+
};
|
|
1057
|
+
const frameworksOn = new Set(scope.frameworks);
|
|
1058
|
+
const controlsOn = new Set(scope.controls);
|
|
1059
|
+
const row$1 = (e) => ({
|
|
1060
|
+
seq: e.seq,
|
|
1061
|
+
at: e.at,
|
|
1062
|
+
tool: e.tool,
|
|
1063
|
+
...e.subject === void 0 ? {} : { subject: e.subject },
|
|
1064
|
+
verdict: e.verdict,
|
|
1065
|
+
...e.rule === void 0 ? {} : { rule: e.rule },
|
|
1066
|
+
...e.session === void 0 ? {} : { session: e.session },
|
|
1067
|
+
agent: agentOf$1(e),
|
|
1068
|
+
hash: e.hash
|
|
1069
|
+
});
|
|
1070
|
+
const frameworks = catalog.frameworks.map((f) => {
|
|
1071
|
+
const selected = frameworksOn.has(f.id);
|
|
1072
|
+
const controls = f.controls.map((c) => {
|
|
1073
|
+
const control = catalog.controls.get(c.key);
|
|
1074
|
+
if (!(selected && controlsOn.has(c.key)) || control === void 0) return {
|
|
1075
|
+
...c,
|
|
1076
|
+
inScope: false,
|
|
1077
|
+
state: "not-in-scope",
|
|
1078
|
+
note: "Not in the selected scope, so not assessed.",
|
|
1079
|
+
rules: [],
|
|
1080
|
+
evidence: {
|
|
1081
|
+
count: 0,
|
|
1082
|
+
rows: []
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
const found = control.evidence(scoped);
|
|
1086
|
+
const trace = control.trace?.(scoped) ?? {
|
|
1087
|
+
rules: [],
|
|
1088
|
+
rows: []
|
|
1089
|
+
};
|
|
1090
|
+
return {
|
|
1091
|
+
...c,
|
|
1092
|
+
inScope: true,
|
|
1093
|
+
state: found.state,
|
|
1094
|
+
note: found.note,
|
|
1095
|
+
rules: trace.rules,
|
|
1096
|
+
evidence: {
|
|
1097
|
+
count: trace.rows.length,
|
|
1098
|
+
rows: trace.rows.slice(-ROW_LIMIT).reverse().map(row$1)
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
});
|
|
1102
|
+
const on$1 = controls.filter((c) => c.inScope);
|
|
1103
|
+
const count = (s) => on$1.filter((c) => c.state === s).length;
|
|
1104
|
+
const { controls: _,...meta } = f;
|
|
1105
|
+
return {
|
|
1106
|
+
...meta,
|
|
1107
|
+
selected,
|
|
1108
|
+
total: controls.length,
|
|
1109
|
+
inScope: on$1.length,
|
|
1110
|
+
covered: count("covered"),
|
|
1111
|
+
partial: count("partial"),
|
|
1112
|
+
absent: count("absent"),
|
|
1113
|
+
controls
|
|
1114
|
+
};
|
|
1115
|
+
});
|
|
1116
|
+
const on = frameworks.flatMap((f) => f.controls.filter((c) => c.inScope));
|
|
1117
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1118
|
+
for (const e of data.entries) {
|
|
1119
|
+
if (e.session === void 0) continue;
|
|
1120
|
+
const s = sessions.get(e.session);
|
|
1121
|
+
if (s === void 0) sessions.set(e.session, {
|
|
1122
|
+
rows: 1,
|
|
1123
|
+
first: e.at,
|
|
1124
|
+
last: e.at
|
|
1125
|
+
});
|
|
1126
|
+
else {
|
|
1127
|
+
s.rows += 1;
|
|
1128
|
+
s.last = e.at;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return {
|
|
1132
|
+
scope,
|
|
1133
|
+
frameworks,
|
|
1134
|
+
totals: {
|
|
1135
|
+
frameworks: frameworks.filter((f) => f.inScope > 0).length,
|
|
1136
|
+
controls: on.length,
|
|
1137
|
+
withEvidence: on.filter((c) => c.state !== "absent").length,
|
|
1138
|
+
covered: on.filter((c) => c.state === "covered").length,
|
|
1139
|
+
partial: on.filter((c) => c.state === "partial").length,
|
|
1140
|
+
absent: on.filter((c) => c.state === "absent").length
|
|
1141
|
+
},
|
|
1142
|
+
dimensions: {
|
|
1143
|
+
agents: [...new Set([options.agent, ...data.entries.map(agentOf$1)])],
|
|
1144
|
+
sessions: [...sessions.entries()].map(([id, s]) => ({
|
|
1145
|
+
id,
|
|
1146
|
+
...s
|
|
1147
|
+
})),
|
|
1148
|
+
environment: options.environment
|
|
1149
|
+
},
|
|
1150
|
+
rows: {
|
|
1151
|
+
admitted: admitted.length,
|
|
1152
|
+
total: data.entries.length
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
//#endregion
|
|
1158
|
+
//#region src/compliance.ts
|
|
1159
|
+
const SCOPE_FILE = ".prae-compliance/scope.json";
|
|
1160
|
+
const CATALOG_FILE = "compliance-catalog.json";
|
|
1161
|
+
/** The record's catalog: built in, plus its own file when it parses. */
|
|
1162
|
+
function catalogFor(dir) {
|
|
1163
|
+
const path = resolve(dir, CATALOG_FILE);
|
|
1164
|
+
if (!existsSync(path)) return { catalog: catalogOf() };
|
|
1165
|
+
let raw;
|
|
1166
|
+
try {
|
|
1167
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
1168
|
+
} catch {
|
|
1169
|
+
return {
|
|
1170
|
+
catalog: catalogOf(),
|
|
1171
|
+
catalogError: `${CATALOG_FILE}: not JSON`
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
const file = parseCatalog(raw);
|
|
1175
|
+
return typeof file === "string" ? {
|
|
1176
|
+
catalog: catalogOf(),
|
|
1177
|
+
catalogError: `${CATALOG_FILE}: ${file}`
|
|
1178
|
+
} : { catalog: catalogOf(file) };
|
|
1179
|
+
}
|
|
1180
|
+
/** The saved scope, or everything built in when none is saved yet. */
|
|
1181
|
+
function scopeFor(dir, catalog) {
|
|
1182
|
+
const path = resolve(dir, SCOPE_FILE);
|
|
1183
|
+
if (!existsSync(path)) return defaultScope(catalog);
|
|
1184
|
+
try {
|
|
1185
|
+
const scope = parseScope(JSON.parse(readFileSync(path, "utf8")));
|
|
1186
|
+
return typeof scope === "string" ? defaultScope(catalog) : scope;
|
|
1187
|
+
} catch {
|
|
1188
|
+
return defaultScope(catalog);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/** The agent the record's discovery names, for rows that name none. */
|
|
1192
|
+
function agentOf(dir) {
|
|
1193
|
+
try {
|
|
1194
|
+
const discovery = JSON.parse(readFileSync(resolve(dir, "discovery.json"), "utf8"));
|
|
1195
|
+
if (typeof discovery.agent?.name === "string") return discovery.agent.name;
|
|
1196
|
+
} catch {}
|
|
1197
|
+
return "this agent";
|
|
1198
|
+
}
|
|
1199
|
+
function assessCompliance(record, data) {
|
|
1200
|
+
const { catalog, catalogError } = catalogFor(record.dir);
|
|
1201
|
+
return {
|
|
1202
|
+
...assess(data, scopeFor(record.dir, catalog), catalog, {
|
|
1203
|
+
agent: agentOf(record.dir),
|
|
1204
|
+
environment: basename(resolve(record.dir))
|
|
1205
|
+
}),
|
|
1206
|
+
saved: existsSync(resolve(record.dir, SCOPE_FILE)),
|
|
1207
|
+
...catalogError === void 0 ? {} : { catalogError }
|
|
858
1208
|
};
|
|
859
1209
|
}
|
|
860
1210
|
|
|
@@ -913,6 +1263,23 @@ function verifyRecord(record, tamper, repair = false) {
|
|
|
913
1263
|
repaired: false,
|
|
914
1264
|
signatures: null
|
|
915
1265
|
};
|
|
1266
|
+
if (tamper === void 0) {
|
|
1267
|
+
const keyPath$1 = resolve(dir, files.publicKey);
|
|
1268
|
+
const publicKey = existsSync(keyPath$1) ? readFileSync(keyPath$1, "utf8") : void 0;
|
|
1269
|
+
const v = verifyGrowing(path, publicKey);
|
|
1270
|
+
return {
|
|
1271
|
+
ok: v.ok,
|
|
1272
|
+
entries: v.entries,
|
|
1273
|
+
problems: [...v.problems],
|
|
1274
|
+
tampered: null,
|
|
1275
|
+
repaired: false,
|
|
1276
|
+
signatures: v.signatures === null || publicKey === void 0 ? null : {
|
|
1277
|
+
checked: v.signatures.checked,
|
|
1278
|
+
failed: [...v.signatures.failed],
|
|
1279
|
+
fingerprint: fingerprint(publicKey)
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
916
1283
|
let text = readFileSync(path, "utf8");
|
|
917
1284
|
let tampered = null;
|
|
918
1285
|
if (tamper !== void 0) {
|
|
@@ -972,6 +1339,45 @@ function verifyRecord(record, tamper, repair = false) {
|
|
|
972
1339
|
signatures
|
|
973
1340
|
};
|
|
974
1341
|
}
|
|
1342
|
+
/** The record's built-in pack and workspace packs, as the gate loads them. */
|
|
1343
|
+
function packSetOf(record) {
|
|
1344
|
+
const { dir, files } = record;
|
|
1345
|
+
const packsDir = resolve(dir, files.packs);
|
|
1346
|
+
const builtinPath = resolve(dir, files.pack);
|
|
1347
|
+
return {
|
|
1348
|
+
packsDir,
|
|
1349
|
+
set: loadPackDir(packsDir, void 0, void 0, existsSync(builtinPath) ? readPackFile(builtinPath, void 0, "builtin") : void 0)
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* The packs in force for a record (PRA-127): the built-in pack and the
|
|
1354
|
+
* workspace's own, read by the gate's loader — each with its origin, version,
|
|
1355
|
+
* sha256 and rules — and every workspace pack the loader rejects, with why.
|
|
1356
|
+
*/
|
|
1357
|
+
function packsOf(record) {
|
|
1358
|
+
const { packsDir, set } = packSetOf(record);
|
|
1359
|
+
return {
|
|
1360
|
+
packs: set.packs.map((p) => ({
|
|
1361
|
+
file: p.file.split("/").at(-1),
|
|
1362
|
+
name: p.pack.pack,
|
|
1363
|
+
version: p.pack.version,
|
|
1364
|
+
...p.pack.description === void 0 ? {} : { description: p.pack.description },
|
|
1365
|
+
...p.pack.owner === void 0 ? {} : { owner: p.pack.owner },
|
|
1366
|
+
sha256: p.sha256,
|
|
1367
|
+
origin: p.origin,
|
|
1368
|
+
rules: p.pack.rules.map((r) => ({
|
|
1369
|
+
id: r.id,
|
|
1370
|
+
...r.title === void 0 ? {} : { title: r.title },
|
|
1371
|
+
kind: r.effect.kind,
|
|
1372
|
+
because: r.effect.because
|
|
1373
|
+
}))
|
|
1374
|
+
})),
|
|
1375
|
+
rejected: set.rejected.filter((r) => r.file !== packsDir).map((r) => ({
|
|
1376
|
+
file: r.file.split("/").at(-1),
|
|
1377
|
+
reason: r.reason
|
|
1378
|
+
}))
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
975
1381
|
/**
|
|
976
1382
|
* Evidence toward each control, from the pack and the ledger as they are
|
|
977
1383
|
* now — the rows the controls package defines, so the console, the exporter and
|
|
@@ -981,40 +1387,56 @@ function verifyRecord(record, tamper, repair = false) {
|
|
|
981
1387
|
function complianceOf(record) {
|
|
982
1388
|
const { dir, files } = record;
|
|
983
1389
|
const ledgerPath = resolve(dir, files.ledger);
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
if (existsSync(packPath)) {
|
|
994
|
-
const parsed = parsePack(readFileSync(packPath, "utf8"));
|
|
995
|
-
if (parsed.ok) pack = parsed.pack;
|
|
996
|
-
}
|
|
1390
|
+
const entries = readLedger(ledgerPath);
|
|
1391
|
+
const grown = existsSync(ledgerPath) ? verifyGrowing(ledgerPath) : void 0;
|
|
1392
|
+
const verification = grown === void 0 ? verifyEntries("") : {
|
|
1393
|
+
ok: grown.ok,
|
|
1394
|
+
entries: grown.entries,
|
|
1395
|
+
problems: grown.problems
|
|
1396
|
+
};
|
|
1397
|
+
const { set } = packSetOf(record);
|
|
1398
|
+
const pack = set.packs.length > 0 ? set.effective : void 0;
|
|
997
1399
|
const data = {
|
|
998
1400
|
...pack === void 0 ? {} : { pack },
|
|
999
1401
|
entries,
|
|
1000
|
-
verification
|
|
1402
|
+
verification,
|
|
1001
1403
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1002
1404
|
};
|
|
1003
|
-
return {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1405
|
+
return {
|
|
1406
|
+
assessment: assessCompliance(record, data),
|
|
1407
|
+
controls: CONTROLS.map((control) => {
|
|
1408
|
+
const found = Object.entries(FRAMEWORKS).find(([, f]) => control.id.startsWith(`${f.short} · `));
|
|
1409
|
+
const code = found === void 0 ? void 0 : control.id.split(" · ")[1];
|
|
1410
|
+
const named = found === void 0 || code === void 0 ? void 0 : found[1].controls[code];
|
|
1411
|
+
const standard = control.standard;
|
|
1412
|
+
const name = named?.name ?? control.name;
|
|
1413
|
+
return {
|
|
1414
|
+
id: code ?? (standard === void 0 ? control.id : control.id.split(" · ")[1] ?? control.id),
|
|
1415
|
+
...name === void 0 ? {} : { name },
|
|
1416
|
+
asks: control.asks,
|
|
1417
|
+
framework: found?.[0] ?? standard?.id ?? "obligation",
|
|
1418
|
+
...found === void 0 ? standard === void 0 ? {} : { frameworkName: standard.name } : { frameworkName: found[1].name },
|
|
1419
|
+
...control.evidence(data)
|
|
1420
|
+
};
|
|
1421
|
+
})
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* The API's version is prae-cli's: every `/api/*` response names it in
|
|
1426
|
+
* `X-PRAE-API-Version`, so a client can tell which contract answered. The
|
|
1427
|
+
* contract follows the package's semver — a breaking change to a route bumps
|
|
1428
|
+
* the major — and a route on its way out carries `Deprecation` and `Sunset`
|
|
1429
|
+
* (RFC 9745 / RFC 8594) for at least one minor release before it goes.
|
|
1430
|
+
*/
|
|
1431
|
+
/**
|
|
1432
|
+
* The most rows `/api/record` sends: the newest ones. The console keeps the
|
|
1433
|
+
* same window (apps/console/src/data/hooks.ts); `trimmed` counts the rest.
|
|
1434
|
+
*/
|
|
1435
|
+
const RECORD_WINDOW = 25e3;
|
|
1436
|
+
const API_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
1437
|
+
/** Name the contract on an API response; call before the status is written. */
|
|
1438
|
+
function apiVersion(res) {
|
|
1439
|
+
res.setHeader("x-prae-api-version", API_VERSION);
|
|
1018
1440
|
}
|
|
1019
1441
|
/**
|
|
1020
1442
|
* The record over HTTP, for the Command Center (apps/console): the same
|
|
@@ -1029,6 +1451,7 @@ function complianceOf(record) {
|
|
|
1029
1451
|
function handleApi(req, res, record) {
|
|
1030
1452
|
const route = (req.url ?? "").split("?")[0] ?? "";
|
|
1031
1453
|
if (!route.startsWith("/api/")) return false;
|
|
1454
|
+
apiVersion(res);
|
|
1032
1455
|
const json = (body, status = 200) => {
|
|
1033
1456
|
res.statusCode = status;
|
|
1034
1457
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
@@ -1043,19 +1466,56 @@ function handleApi(req, res, record) {
|
|
|
1043
1466
|
connection: "keep-alive"
|
|
1044
1467
|
});
|
|
1045
1468
|
res.write(": connected\n\n");
|
|
1046
|
-
|
|
1469
|
+
const ledgerPath = resolve(dir, files.ledger);
|
|
1470
|
+
const size = () => existsSync(ledgerPath) ? statSync(ledgerPath).size : 0;
|
|
1471
|
+
let offset = (() => {
|
|
1472
|
+
if (!existsSync(ledgerPath)) return 0;
|
|
1473
|
+
return readFileSync(ledgerPath).lastIndexOf(10) + 1;
|
|
1474
|
+
})();
|
|
1047
1475
|
const push = () => {
|
|
1048
|
-
const
|
|
1049
|
-
if (
|
|
1050
|
-
|
|
1051
|
-
|
|
1476
|
+
const end = size();
|
|
1477
|
+
if (end < offset) {
|
|
1478
|
+
offset = 0;
|
|
1479
|
+
res.write("event: reset\ndata: {}\n\n");
|
|
1480
|
+
}
|
|
1481
|
+
const fresh = [];
|
|
1482
|
+
if (end > offset) {
|
|
1483
|
+
const bytes = Buffer.alloc(end - offset);
|
|
1484
|
+
const fd = openSync(ledgerPath, "r");
|
|
1485
|
+
try {
|
|
1486
|
+
readSync(fd, bytes, 0, bytes.length, offset);
|
|
1487
|
+
} finally {
|
|
1488
|
+
closeSync(fd);
|
|
1489
|
+
}
|
|
1490
|
+
let at = 0;
|
|
1491
|
+
for (;;) {
|
|
1492
|
+
const nl = bytes.indexOf(10, at);
|
|
1493
|
+
if (nl === -1) break;
|
|
1494
|
+
const l = bytes.subarray(at, nl).toString("utf8");
|
|
1495
|
+
if (l.trim() !== "") try {
|
|
1496
|
+
fresh.push(JSON.parse(l));
|
|
1497
|
+
} catch {
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
at = nl + 1;
|
|
1501
|
+
}
|
|
1502
|
+
offset += at;
|
|
1503
|
+
}
|
|
1052
1504
|
if (fresh.length > 0) res.write(`data: ${JSON.stringify(fresh)}\n\n`);
|
|
1053
1505
|
else res.write(": keepalive\n\n");
|
|
1054
1506
|
};
|
|
1055
|
-
|
|
1507
|
+
let soon;
|
|
1508
|
+
const schedule = () => {
|
|
1509
|
+
soon ??= setTimeout(() => {
|
|
1510
|
+
soon = void 0;
|
|
1511
|
+
push();
|
|
1512
|
+
}, 250);
|
|
1513
|
+
};
|
|
1514
|
+
const watcher = existsSync(dir) ? watch(dir, { persistent: false }, schedule) : void 0;
|
|
1056
1515
|
const keep = setInterval(push, 2e3);
|
|
1057
1516
|
res.on("close", () => {
|
|
1058
1517
|
clearInterval(keep);
|
|
1518
|
+
if (soon !== void 0) clearTimeout(soon);
|
|
1059
1519
|
watcher?.close();
|
|
1060
1520
|
});
|
|
1061
1521
|
return true;
|
|
@@ -1071,15 +1531,40 @@ function handleApi(req, res, record) {
|
|
|
1071
1531
|
json(complianceOf(record));
|
|
1072
1532
|
return true;
|
|
1073
1533
|
}
|
|
1534
|
+
if (route === "/api/packs") {
|
|
1535
|
+
json(packsOf(record));
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1074
1538
|
if (route === "/api/redteam") {
|
|
1075
1539
|
json(readRedteam(dir, files));
|
|
1076
1540
|
return true;
|
|
1077
1541
|
}
|
|
1542
|
+
if (route === "/api/status") {
|
|
1543
|
+
json({
|
|
1544
|
+
dir,
|
|
1545
|
+
files: Object.fromEntries(Object.entries(files).map(([name, file]) => {
|
|
1546
|
+
const path = resolve(dir, file);
|
|
1547
|
+
if (!existsSync(path)) return [name, null];
|
|
1548
|
+
const stat$1 = statSync(path);
|
|
1549
|
+
return [name, {
|
|
1550
|
+
size: stat$1.size,
|
|
1551
|
+
mtime: stat$1.mtimeMs
|
|
1552
|
+
}];
|
|
1553
|
+
}))
|
|
1554
|
+
});
|
|
1555
|
+
return true;
|
|
1556
|
+
}
|
|
1078
1557
|
const snapshot = readRecord(dir, files);
|
|
1079
1558
|
switch (route) {
|
|
1080
|
-
case "/api/record":
|
|
1081
|
-
|
|
1559
|
+
case "/api/record": {
|
|
1560
|
+
const over = snapshot.ledger.length - RECORD_WINDOW;
|
|
1561
|
+
json(over > 0 ? {
|
|
1562
|
+
...snapshot,
|
|
1563
|
+
ledger: snapshot.ledger.slice(over),
|
|
1564
|
+
trimmed: over
|
|
1565
|
+
} : snapshot);
|
|
1082
1566
|
break;
|
|
1567
|
+
}
|
|
1083
1568
|
case "/api/ledger":
|
|
1084
1569
|
json(snapshot.ledger);
|
|
1085
1570
|
break;
|
|
@@ -1095,20 +1580,6 @@ function handleApi(req, res, record) {
|
|
|
1095
1580
|
case "/api/pack":
|
|
1096
1581
|
json(snapshot.pack);
|
|
1097
1582
|
break;
|
|
1098
|
-
case "/api/status":
|
|
1099
|
-
json({
|
|
1100
|
-
dir,
|
|
1101
|
-
files: Object.fromEntries(Object.entries(files).map(([name, file]) => {
|
|
1102
|
-
const path = resolve(dir, file);
|
|
1103
|
-
if (!existsSync(path)) return [name, null];
|
|
1104
|
-
const stat$1 = statSync(path);
|
|
1105
|
-
return [name, {
|
|
1106
|
-
size: stat$1.size,
|
|
1107
|
-
mtime: stat$1.mtimeMs
|
|
1108
|
-
}];
|
|
1109
|
-
}))
|
|
1110
|
-
});
|
|
1111
|
-
break;
|
|
1112
1583
|
default: json({ error: "not found" }, 404);
|
|
1113
1584
|
}
|
|
1114
1585
|
return true;
|
|
@@ -1125,6 +1596,7 @@ function serveConsole(root, port, record) {
|
|
|
1125
1596
|
res.end();
|
|
1126
1597
|
return;
|
|
1127
1598
|
}
|
|
1599
|
+
if ((req.url ?? "").startsWith("/api/")) apiVersion(res);
|
|
1128
1600
|
if (record !== void 0 && handleApi(req, res, record)) return;
|
|
1129
1601
|
const target = safePath(root, req.url ?? "/");
|
|
1130
1602
|
if (target === void 0) {
|
|
@@ -1169,6 +1641,12 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1169
1641
|
prae check <pack.json>
|
|
1170
1642
|
Validate a policy pack and summarise what it enforces.
|
|
1171
1643
|
|
|
1644
|
+
prae check <dir> [--policy-key <policy.pub.pem>]
|
|
1645
|
+
Validate a workspace's packs (e.g. .prae/packs) exactly as the gate
|
|
1646
|
+
loads them: every *.prae.json / .yaml in name order, each rejection
|
|
1647
|
+
with its reason, and, with --policy-key, each .sig checked. Exits
|
|
1648
|
+
non-zero if any pack would be rejected.
|
|
1649
|
+
|
|
1172
1650
|
prae explain <pack.json> <tool> [key=value ...]
|
|
1173
1651
|
Judge one call against the pack and print the trace. Nothing is executed.
|
|
1174
1652
|
|
|
@@ -1195,6 +1673,14 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1195
1673
|
--checkpoint, also check that nothing before that checkpoint's head
|
|
1196
1674
|
has been removed or rewritten since it was taken.
|
|
1197
1675
|
|
|
1676
|
+
prae chain <ledger.jsonl> --pack <pack.json> [--guardians <guardians.json>]
|
|
1677
|
+
[--key <public.pem>] [--json]
|
|
1678
|
+
The Attack Chain Integrity Matrix: each attack-chain row from the
|
|
1679
|
+
attempted action to its seal — capability, Guardian, rule, decision,
|
|
1680
|
+
whether it ran, the stage and why, the hash and the signature. The
|
|
1681
|
+
stage is classified from the sealed row and the pack, never typed in.
|
|
1682
|
+
Exits non-zero if the chain or (with --key) any signature fails.
|
|
1683
|
+
|
|
1198
1684
|
prae checkpoint <ledger.jsonl> [--key <private.pem>] [--out <file>]
|
|
1199
1685
|
Record the ledger's length and head, signed, to keep somewhere the
|
|
1200
1686
|
ledger's machine cannot write. A chain proves no row inside the file
|
|
@@ -1252,13 +1738,31 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
1252
1738
|
Read the instruction files at <dir> and propose the rules they imply.
|
|
1253
1739
|
Proposals only: an existing pack is never modified.
|
|
1254
1740
|
|
|
1255
|
-
prae init [--pack <file>] [--profile <dir>] [--ledger <file>]
|
|
1741
|
+
prae init [--pack <file> | --pack-dir <dir>] [--profile <dir>] [--ledger <file>]
|
|
1256
1742
|
(no --pack mounts the built-in baseline of 8 high-risk rules)
|
|
1257
1743
|
[--approver <name>] [--link] [--shadow] [--apply]
|
|
1258
1744
|
Compose the overlay row that mounts PRAE with a pack. Prints the
|
|
1259
1745
|
diff; writes only with --apply. --link points the profile at the
|
|
1260
1746
|
pack file instead of copying it in, so edits hot-reload; --shadow
|
|
1261
1747
|
mounts in shadow mode: every verdict sealed, every call let through.
|
|
1748
|
+
--pack-dir adds the workspace's own packs (e.g. .prae/packs) beside
|
|
1749
|
+
that pack, loaded and watched as a set; a set that would not load is
|
|
1750
|
+
not mounted (see \`prae check <dir>\`).
|
|
1751
|
+
|
|
1752
|
+
prae init claude-code (--settings <file> | --scope user|project)
|
|
1753
|
+
[--shadow | --enforce] [--pack <file>] [--ledger <file>]
|
|
1754
|
+
[--dir <dir>] [--apply] [--remove]
|
|
1755
|
+
Register PRAE as a Claude Code PreToolUse hook: every tool call,
|
|
1756
|
+
built-in or MCP, judged by the pack and sealed in a signed ledger
|
|
1757
|
+
(default ~/.prae/claude-code/). Shadow (the default) records and
|
|
1758
|
+
never blocks; --enforce returns deny/ask to Claude Code. Prints the
|
|
1759
|
+
settings diff; writes only with --apply. --remove takes out PRAE's
|
|
1760
|
+
own entry and nothing else. See docs/pilot-claude-code.md.
|
|
1761
|
+
|
|
1762
|
+
prae hook claude-code [--shadow | --enforce] --pack <file> --ledger <file>
|
|
1763
|
+
[--key <private.pem>] [--settings <file>]
|
|
1764
|
+
The command that settings entry runs: reads one PreToolUse payload on
|
|
1765
|
+
stdin and answers through the hook's output contract.
|
|
1262
1766
|
`;
|
|
1263
1767
|
function fail(message) {
|
|
1264
1768
|
console.error(`prae: ${message}`);
|
|
@@ -1279,7 +1783,25 @@ async function loadPack(path) {
|
|
|
1279
1783
|
}
|
|
1280
1784
|
return result.pack;
|
|
1281
1785
|
}
|
|
1282
|
-
async function
|
|
1786
|
+
async function checkDir(dir, keyPath) {
|
|
1787
|
+
let key;
|
|
1788
|
+
if (keyPath !== void 0) try {
|
|
1789
|
+
key = await readFile(keyPath, "utf8");
|
|
1790
|
+
} catch {
|
|
1791
|
+
return fail(`cannot read ${keyPath}`);
|
|
1792
|
+
}
|
|
1793
|
+
const set = loadPackDir(dir, key);
|
|
1794
|
+
for (const p of set.packs) {
|
|
1795
|
+
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}`}`);
|
|
1796
|
+
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}`);
|
|
1797
|
+
}
|
|
1798
|
+
for (const r of set.rejected) console.error(`REJECTED ${r.file.split("/").at(-1)}\n ${r.reason}`);
|
|
1799
|
+
if (set.packs.length === 0) console.error("\nNo valid pack: the gate would refuse every call until one loads.");
|
|
1800
|
+
if (set.rejected.length > 0) process.exit(1);
|
|
1801
|
+
console.log(`\n${set.packs.length} pack${set.packs.length === 1 ? "" : "s"} valid · in force as ${set.effective.pack} ${set.effective.version}`);
|
|
1802
|
+
}
|
|
1803
|
+
async function check(path, argv = []) {
|
|
1804
|
+
if (existsSync(path) && statSync(path).isDirectory()) return checkDir(path, flag(argv, "policy-key"));
|
|
1283
1805
|
const pack = await loadPack(path);
|
|
1284
1806
|
console.log(`${pack.pack} ${pack.version} — valid`);
|
|
1285
1807
|
const baseline = pack.baseline?.capabilities;
|
|
@@ -1300,7 +1822,7 @@ async function check(path) {
|
|
|
1300
1822
|
* from the findings, so there is nothing here to be careful about printing.
|
|
1301
1823
|
*/
|
|
1302
1824
|
async function scanFile(path, packPath, json) {
|
|
1303
|
-
const { scan } = await import("./src-
|
|
1825
|
+
const { scan } = await import("./src-DMoYsdfo.js");
|
|
1304
1826
|
let content;
|
|
1305
1827
|
try {
|
|
1306
1828
|
content = await readFile(path, "utf8");
|
|
@@ -1340,13 +1862,13 @@ async function scanFile(path, packPath, json) {
|
|
|
1340
1862
|
* not come up inspecting nothing.
|
|
1341
1863
|
*/
|
|
1342
1864
|
async function mcp(root, packPath, ledgerPath) {
|
|
1343
|
-
const { serveMcp } = await import("./mcp-
|
|
1865
|
+
const { serveMcp } = await import("./mcp-D521EoP6.js");
|
|
1344
1866
|
const { resolve: resolve$1 } = await import("node:path");
|
|
1345
1867
|
const bundled = [join(import.meta.dirname, "secret-catcher.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "secret-catcher.prae.json")].find((candidate) => existsSync(candidate));
|
|
1346
1868
|
const chosen = packPath ?? bundled;
|
|
1347
1869
|
if (chosen === void 0) return fail("mcp needs --pack <file>; no bundled secret-catcher found");
|
|
1348
1870
|
const pack = await loadPack(chosen);
|
|
1349
|
-
const { Ledger: Ledger$1 } = await import("./src-
|
|
1871
|
+
const { Ledger: Ledger$1 } = await import("./src-wG36kT6K.js");
|
|
1350
1872
|
const ledger = ledgerPath === void 0 ? void 0 : Ledger$1.open(ledgerPath);
|
|
1351
1873
|
console.error(`prae mcp: serving ${resolve$1(root)} under ${pack.pack} ${pack.version}` + (ledgerPath === void 0 ? " (no ledger)" : ` · ledger ${ledgerPath}`));
|
|
1352
1874
|
await serveMcp({
|
|
@@ -1398,7 +1920,7 @@ async function explain(path, tool, pairs) {
|
|
|
1398
1920
|
* a key that arrived beside it is checked against nothing.
|
|
1399
1921
|
*/
|
|
1400
1922
|
async function keygen(dir) {
|
|
1401
|
-
const { generateKeypair, fingerprint: fingerprint$1 } = await import("./src-
|
|
1923
|
+
const { generateKeypair, fingerprint: fingerprint$1 } = await import("./src-wG36kT6K.js");
|
|
1402
1924
|
const { writeFileSync: writeFileSync$1 } = await import("node:fs");
|
|
1403
1925
|
const keys = generateKeypair();
|
|
1404
1926
|
const priv = join(dir, "prae-signing.pem");
|
|
@@ -1421,7 +1943,7 @@ async function keygen(dir) {
|
|
|
1421
1943
|
async function keygenSealed(role, dir) {
|
|
1422
1944
|
if (role !== "operator" && role !== "policy" && role !== "ledger") fail("--role is operator, policy or ledger");
|
|
1423
1945
|
const stem = role === "ledger" ? "prae-signing" : role;
|
|
1424
|
-
const { sealedKeypair, fingerprint: fingerprint$1 } = await import("./src-
|
|
1946
|
+
const { sealedKeypair, fingerprint: fingerprint$1 } = await import("./src-wG36kT6K.js");
|
|
1425
1947
|
const { writeFileSync: writeFileSync$1 } = await import("node:fs");
|
|
1426
1948
|
const keys = sealedKeypair(process.env.PRAE_KEY_PASSPHRASE ?? await askHidden("passphrase: "));
|
|
1427
1949
|
const pub = join(dir, `${stem}.pub.pem`);
|
|
@@ -1434,11 +1956,11 @@ async function keygenSealed(role, dir) {
|
|
|
1434
1956
|
}
|
|
1435
1957
|
/** A detached signature over a pack's bytes (PRA-107). */
|
|
1436
1958
|
async function signPack(packPath, keyPath) {
|
|
1437
|
-
const { signBytes, unsealPrivateKey, fingerprint: fingerprint$1 } = await import("./src-
|
|
1438
|
-
const { createPublicKey } = await import("node:crypto");
|
|
1959
|
+
const { signBytes, unsealPrivateKey, fingerprint: fingerprint$1 } = await import("./src-wG36kT6K.js");
|
|
1960
|
+
const { createPublicKey: createPublicKey$1 } = await import("node:crypto");
|
|
1439
1961
|
const bytes = readFileSync(packPath);
|
|
1440
|
-
const parsed = parsePack(bytes.toString("utf8"));
|
|
1441
|
-
if (!parsed.ok) fail(`${packPath} does not parse; nothing signed`);
|
|
1962
|
+
const parsed$1 = parsePack(bytes.toString("utf8"));
|
|
1963
|
+
if (!parsed$1.ok) fail(`${packPath} does not parse; nothing signed`);
|
|
1442
1964
|
const passphrase = process.env.PRAE_KEY_PASSPHRASE ?? await askHidden("passphrase: ");
|
|
1443
1965
|
let key;
|
|
1444
1966
|
try {
|
|
@@ -1447,11 +1969,11 @@ async function signPack(packPath, keyPath) {
|
|
|
1447
1969
|
fail("wrong passphrase, or not a sealed key");
|
|
1448
1970
|
}
|
|
1449
1971
|
await writeFile(`${packPath}.sig`, `${signBytes(bytes, key)}\n`);
|
|
1450
|
-
const pub = createPublicKey(key).export({
|
|
1972
|
+
const pub = createPublicKey$1(key).export({
|
|
1451
1973
|
type: "spki",
|
|
1452
1974
|
format: "pem"
|
|
1453
1975
|
});
|
|
1454
|
-
console.log(`signed ${packPath}.sig (${parsed.pack.pack} ${parsed.pack.version})`);
|
|
1976
|
+
console.log(`signed ${packPath}.sig (${parsed$1.pack.pack} ${parsed$1.pack.version})`);
|
|
1455
1977
|
console.log(`policy key ${fingerprint$1(pub.toString())}`);
|
|
1456
1978
|
}
|
|
1457
1979
|
/** Read a line from the terminal without echoing it. */
|
|
@@ -1479,11 +2001,11 @@ function askHidden(prompt) {
|
|
|
1479
2001
|
});
|
|
1480
2002
|
}
|
|
1481
2003
|
async function receipt(path, seq, keyPath, out) {
|
|
1482
|
-
const { Ledger: Ledger$1, makeReceipt, fingerprint: fingerprint$1 } = await import("./src-
|
|
2004
|
+
const { Ledger: Ledger$1, makeReceipt, fingerprint: fingerprint$1 } = await import("./src-wG36kT6K.js");
|
|
1483
2005
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1484
|
-
const { createPublicKey } = await import("node:crypto");
|
|
2006
|
+
const { createPublicKey: createPublicKey$1 } = await import("node:crypto");
|
|
1485
2007
|
const privateKey = readFileSync$1(keyPath, "utf8");
|
|
1486
|
-
const publicKey = createPublicKey(privateKey).export({
|
|
2008
|
+
const publicKey = createPublicKey$1(privateKey).export({
|
|
1487
2009
|
type: "spki",
|
|
1488
2010
|
format: "pem"
|
|
1489
2011
|
}).toString();
|
|
@@ -1509,14 +2031,14 @@ async function receipt(path, seq, keyPath, out) {
|
|
|
1509
2031
|
* file and sets the exit code, so scripts can branch on it.
|
|
1510
2032
|
*/
|
|
1511
2033
|
async function verifyReceiptCmd(path) {
|
|
1512
|
-
const { checkReceipt } = await import("./receipt-check-
|
|
1513
|
-
let parsed;
|
|
2034
|
+
const { checkReceipt } = await import("./receipt-check-CUuEjGHL.js");
|
|
2035
|
+
let parsed$1;
|
|
1514
2036
|
try {
|
|
1515
|
-
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
2037
|
+
parsed$1 = JSON.parse(await readFile(path, "utf8"));
|
|
1516
2038
|
} catch {
|
|
1517
2039
|
return fail(`cannot read a receipt from ${path}`);
|
|
1518
2040
|
}
|
|
1519
|
-
const result = checkReceipt(parsed);
|
|
2041
|
+
const result = checkReceipt(parsed$1);
|
|
1520
2042
|
for (const line of result.lines) if (result.ok) console.log(line);
|
|
1521
2043
|
else console.error(line);
|
|
1522
2044
|
if (!result.ok) process.exit(1);
|
|
@@ -1534,14 +2056,14 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
1534
2056
|
} catch {
|
|
1535
2057
|
return fail(`cannot read ${suitePath}`);
|
|
1536
2058
|
}
|
|
1537
|
-
let parsed;
|
|
2059
|
+
let parsed$1;
|
|
1538
2060
|
try {
|
|
1539
|
-
parsed = JSON.parse(raw);
|
|
2061
|
+
parsed$1 = JSON.parse(raw);
|
|
1540
2062
|
} catch (error) {
|
|
1541
2063
|
return fail(`${suitePath} is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
1542
2064
|
}
|
|
1543
|
-
const { parseSuite, renderSuite, runSuite } = await import("./test-run-
|
|
1544
|
-
const suite = parseSuite(parsed);
|
|
2065
|
+
const { parseSuite, renderSuite, runSuite } = await import("./test-run-C8dHjIE2.js");
|
|
2066
|
+
const suite = parseSuite(parsed$1);
|
|
1545
2067
|
if (!suite.ok) {
|
|
1546
2068
|
console.error(`prae: ${suitePath} is not a valid test suite`);
|
|
1547
2069
|
for (const message of suite.errors) console.error(` ${message}`);
|
|
@@ -1551,7 +2073,7 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
1551
2073
|
if (chosen === void 0) return fail("test needs a pack: set `pack` in the suite or pass --pack");
|
|
1552
2074
|
const pack = await loadPack(chosen);
|
|
1553
2075
|
const { capabilityForTool } = await import("capmark");
|
|
1554
|
-
const { scan } = await import("./src-
|
|
2076
|
+
const { scan } = await import("./src-DMoYsdfo.js");
|
|
1555
2077
|
const inspect = pack.rules.some((rule) => rule.when.secrets !== void 0) ? (path) => {
|
|
1556
2078
|
try {
|
|
1557
2079
|
return scan(readFileSync(path, "utf8"));
|
|
@@ -1567,7 +2089,7 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
1567
2089
|
else for (const line of renderSuite(result)) console.log(line);
|
|
1568
2090
|
if (!result.ok) process.exit(1);
|
|
1569
2091
|
}
|
|
1570
|
-
async function verify(path, keyPath, checkpointPath) {
|
|
2092
|
+
async function verify$1(path, keyPath, checkpointPath) {
|
|
1571
2093
|
let ledger;
|
|
1572
2094
|
try {
|
|
1573
2095
|
ledger = Ledger.open(path);
|
|
@@ -1580,14 +2102,14 @@ async function verify(path, keyPath, checkpointPath) {
|
|
|
1580
2102
|
const last = entries.at(-1);
|
|
1581
2103
|
console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
|
|
1582
2104
|
if (checkpointPath !== void 0) {
|
|
1583
|
-
const { checkCheckpoint } = await import("./checkpoint-
|
|
2105
|
+
const { checkCheckpoint } = await import("./checkpoint-BqJ8xwky.js");
|
|
1584
2106
|
const publicKey = keyPath === void 0 ? void 0 : readFileSync(keyPath, "utf8");
|
|
1585
2107
|
const verdict = checkCheckpoint(entries, JSON.parse(readFileSync(checkpointPath, "utf8")), publicKey);
|
|
1586
2108
|
for (const line of verdict.lines) (verdict.ok ? console.log : console.error)(`checkpoint: ${line}`);
|
|
1587
2109
|
if (!verdict.ok) process.exitCode = 1;
|
|
1588
2110
|
}
|
|
1589
2111
|
if (keyPath !== void 0) {
|
|
1590
|
-
const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-
|
|
2112
|
+
const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-wG36kT6K.js");
|
|
1591
2113
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1592
2114
|
const publicKey = readFileSync$1(keyPath, "utf8");
|
|
1593
2115
|
let signed = 0;
|
|
@@ -1665,7 +2187,7 @@ async function report(path, out, packPath) {
|
|
|
1665
2187
|
* that step is a proposal a human approves, and it starts from this list.
|
|
1666
2188
|
*/
|
|
1667
2189
|
async function instructions(from, asJson) {
|
|
1668
|
-
const { ingest } = await import("./src-
|
|
2190
|
+
const { ingest } = await import("./src-CDYyZyeU.js");
|
|
1669
2191
|
const result = ingest(from);
|
|
1670
2192
|
if (asJson) {
|
|
1671
2193
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -1701,7 +2223,7 @@ async function instructions(from, asJson) {
|
|
|
1701
2223
|
* on purpose rather than something that happened to their policy.
|
|
1702
2224
|
*/
|
|
1703
2225
|
async function propose(from, options) {
|
|
1704
|
-
const [{ ingest }, { extract }] = await Promise.all([import("./src-
|
|
2226
|
+
const [{ ingest }, { extract }] = await Promise.all([import("./src-CDYyZyeU.js"), import("./src-BlmsTGdY.js")]);
|
|
1705
2227
|
const read = ingest(from);
|
|
1706
2228
|
if (read.files.length === 0) {
|
|
1707
2229
|
console.log(`No instruction files found at or above ${from}.`);
|
|
@@ -1761,9 +2283,9 @@ async function consoleOut(path, argv) {
|
|
|
1761
2283
|
const instructionsDir = flag(argv, "instructions");
|
|
1762
2284
|
if (instructionsDir !== void 0) {
|
|
1763
2285
|
const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
|
|
1764
|
-
import("./src-
|
|
1765
|
-
import("./src-
|
|
1766
|
-
import("./instructions-
|
|
2286
|
+
import("./src-CDYyZyeU.js"),
|
|
2287
|
+
import("./src-BlmsTGdY.js"),
|
|
2288
|
+
import("./instructions-DihPiiC2.js")
|
|
1767
2289
|
]);
|
|
1768
2290
|
const read = ingest(instructionsDir);
|
|
1769
2291
|
instructionsView = renderInstructions(data, read, extract(read.statements));
|
|
@@ -1804,9 +2326,12 @@ async function consoleOut(path, argv) {
|
|
|
1804
2326
|
console.log(`record at ${url}api/record · events at ${url}api/events`);
|
|
1805
2327
|
console.log("loopback only, read-only. ctrl-c to stop.");
|
|
1806
2328
|
}
|
|
2329
|
+
/** The bundled baseline: beside the built code, or in examples/ in source. */
|
|
2330
|
+
function bundledBaseline() {
|
|
2331
|
+
return [join(import.meta.dirname, "prae-baseline.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "prae-baseline.prae.json")].find((candidate) => existsSync(candidate));
|
|
2332
|
+
}
|
|
1807
2333
|
async function init(argv) {
|
|
1808
|
-
const
|
|
1809
|
-
const packPath = flag(argv, "pack") ?? bundledBaseline;
|
|
2334
|
+
const packPath = flag(argv, "pack") ?? bundledBaseline();
|
|
1810
2335
|
if (packPath === void 0) fail("init needs --pack <file>; no bundled baseline found");
|
|
1811
2336
|
if (flag(argv, "pack") === void 0) {
|
|
1812
2337
|
console.log("No --pack given; mounting the built-in baseline (8 rules).");
|
|
@@ -1814,14 +2339,29 @@ async function init(argv) {
|
|
|
1814
2339
|
}
|
|
1815
2340
|
const pack = await loadPack(packPath);
|
|
1816
2341
|
const source = await readFile(packPath, "utf8");
|
|
2342
|
+
const link = argv.includes("--link");
|
|
2343
|
+
const builtin = link ? { packFile: resolve(packPath) } : { pack: source };
|
|
2344
|
+
const builtinLine = `${pack.pack} ${pack.version} · ${pack.rules.length} rules${link ? ` · linked, edits hot-reload (${resolve(packPath)})` : ""}`;
|
|
2345
|
+
const packDirFlag = flag(argv, "pack-dir");
|
|
2346
|
+
if (packDirFlag === void 0) return writeRow(argv, builtin, builtinLine);
|
|
2347
|
+
const packDir = resolve(packDirFlag);
|
|
2348
|
+
const set = loadPackDir(packDir, void 0, void 0, readPackFile(packPath, void 0, "builtin"));
|
|
2349
|
+
for (const r of set.rejected) console.error(`REJECTED ${r.file.split("/").at(-1)}: ${r.reason}`);
|
|
2350
|
+
if (set.rejected.length > 0) fail(`not mounting ${packDir}: fix the packs above (prae check ${packDir})`);
|
|
2351
|
+
const custom = set.packs.filter((p) => p.origin === "workspace");
|
|
2352
|
+
return writeRow(argv, {
|
|
2353
|
+
...builtin,
|
|
2354
|
+
packDir
|
|
2355
|
+
}, `${builtinLine}\n + ${custom.map((p) => `${p.pack.pack} ${p.pack.version}`).join(", ")} · workspace, watched (${packDir})`);
|
|
2356
|
+
}
|
|
2357
|
+
async function writeRow(argv, source, packLine) {
|
|
1817
2358
|
const profileDir = flag(argv, "profile");
|
|
1818
2359
|
const profile = profileDir === void 0 ? defaultProfile() : profileAt(profileDir);
|
|
1819
2360
|
const ledger = flag(argv, "ledger");
|
|
1820
2361
|
const approver = flag(argv, "approver");
|
|
1821
|
-
const link = argv.includes("--link");
|
|
1822
2362
|
const shadow = argv.includes("--shadow");
|
|
1823
2363
|
const row$1 = praeRow({
|
|
1824
|
-
...
|
|
2364
|
+
...source,
|
|
1825
2365
|
...shadow ? { mode: "shadow" } : {},
|
|
1826
2366
|
...ledger === void 0 ? {} : { ledger },
|
|
1827
2367
|
...approver === void 0 ? {} : { approver }
|
|
@@ -1834,7 +2374,7 @@ async function init(argv) {
|
|
|
1834
2374
|
}
|
|
1835
2375
|
console.log(`profile ${profile.dir}`);
|
|
1836
2376
|
console.log(`overlay ${plan.patchPath}`);
|
|
1837
|
-
console.log(`pack ${
|
|
2377
|
+
console.log(`pack ${packLine}`);
|
|
1838
2378
|
if (shadow) console.log("mode shadow · every verdict sealed, every call proceeds");
|
|
1839
2379
|
if (ledger !== void 0) console.log(`ledger ${ledger}`);
|
|
1840
2380
|
if (plan.findings.length > 0) {
|
|
@@ -1861,8 +2401,55 @@ async function init(argv) {
|
|
|
1861
2401
|
return fail(error instanceof Error ? error.message : String(error));
|
|
1862
2402
|
}
|
|
1863
2403
|
}
|
|
2404
|
+
/** `prae chain`: the Attack Chain Integrity Matrix, from the record alone (PRA-128). */
|
|
2405
|
+
async function chainMatrix(path, argv) {
|
|
2406
|
+
const packPath = flag(argv, "pack");
|
|
2407
|
+
if (packPath === void 0) fail("chain needs --pack <pack.json>");
|
|
2408
|
+
const pack = await loadPack(packPath);
|
|
2409
|
+
let ledger;
|
|
2410
|
+
try {
|
|
2411
|
+
ledger = Ledger.open(path);
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
2414
|
+
}
|
|
2415
|
+
const entries = ledger.read();
|
|
2416
|
+
const chainOk = ledger.verify().ok;
|
|
2417
|
+
const guardiansPath = flag(argv, "guardians");
|
|
2418
|
+
const catalogue = guardiansPath === void 0 ? [] : JSON.parse(readFileSync(guardiansPath, "utf8")).guardians;
|
|
2419
|
+
const guardianOf = (rule) => rule === void 0 ? void 0 : catalogue.find((g) => g.rules.includes(rule) || (g.armed ?? []).includes(rule))?.name;
|
|
2420
|
+
const keyPath = flag(argv, "key");
|
|
2421
|
+
const { verifySignature: verifySignature$1 } = await import("./src-wG36kT6K.js");
|
|
2422
|
+
const publicKey = keyPath === void 0 ? void 0 : readFileSync(keyPath, "utf8");
|
|
2423
|
+
const matrix = integrityMatrix(entries, pack, guardianOf).map((m) => {
|
|
2424
|
+
const entry = entries.find((e) => e.seq === m.seq);
|
|
2425
|
+
return {
|
|
2426
|
+
...m,
|
|
2427
|
+
verified: publicKey === void 0 || entry?.sig === void 0 ? void 0 : verifySignature$1(entry, entry.sig, publicKey)
|
|
2428
|
+
};
|
|
2429
|
+
});
|
|
2430
|
+
const sigFailed = matrix.some((m) => m.verified === false);
|
|
2431
|
+
if (argv.includes("--json")) console.log(JSON.stringify({
|
|
2432
|
+
chain: chainOk,
|
|
2433
|
+
rows: matrix
|
|
2434
|
+
}, null, 2));
|
|
2435
|
+
else {
|
|
2436
|
+
for (const m of matrix) {
|
|
2437
|
+
console.log(`#${m.seq} ${m.stage}`);
|
|
2438
|
+
console.log(` attempted ${m.action}${m.target ? ` ${m.target}` : ""}`);
|
|
2439
|
+
console.log(` capability ${m.capability ?? "—"}`);
|
|
2440
|
+
console.log(` guardian ${m.guardian ?? "—"}`);
|
|
2441
|
+
console.log(` rule ${m.rule === void 0 ? "— (no rule matched)" : m.ruleRole === "recorded" ? `${m.rule} (matched and recorded; did not decide)` : m.rule}`);
|
|
2442
|
+
console.log(` decision ${m.decision} · ${m.execution}`);
|
|
2443
|
+
console.log(` basis ${m.basis}`);
|
|
2444
|
+
if (m.mismatch !== void 0) console.log(` mismatch row #${m.mismatch} (sealed link)`);
|
|
2445
|
+
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"}`);
|
|
2446
|
+
}
|
|
2447
|
+
console.log(`\n${matrix.length} attack-chain rows · chain ${chainOk ? "intact" : "BROKEN"}${publicKey === void 0 ? "" : sigFailed ? " · signatures FAIL" : " · signatures verify"}`);
|
|
2448
|
+
}
|
|
2449
|
+
if (!chainOk || sigFailed) process.exitCode = 1;
|
|
2450
|
+
}
|
|
1864
2451
|
async function checkpoint(path, keyPath, out) {
|
|
1865
|
-
const { makeCheckpoint } = await import("./checkpoint-
|
|
2452
|
+
const { makeCheckpoint } = await import("./checkpoint-BqJ8xwky.js");
|
|
1866
2453
|
let ledger;
|
|
1867
2454
|
try {
|
|
1868
2455
|
ledger = Ledger.open(path);
|
|
@@ -1883,9 +2470,9 @@ async function checkpoint(path, keyPath, out) {
|
|
|
1883
2470
|
}
|
|
1884
2471
|
}
|
|
1885
2472
|
async function exportLedger(path, options) {
|
|
1886
|
-
const { toOcsf, ndjson, sendToHec } = await import("./ocsf-
|
|
1887
|
-
const { createRequire } = await import("node:module");
|
|
1888
|
-
const { version } = createRequire(import.meta.url)("../package.json");
|
|
2473
|
+
const { toOcsf, ndjson, sendToHec } = await import("./ocsf-DF9cMiQJ.js");
|
|
2474
|
+
const { createRequire: createRequire$1 } = await import("node:module");
|
|
2475
|
+
const { version } = createRequire$1(import.meta.url)("../package.json");
|
|
1889
2476
|
let ledger;
|
|
1890
2477
|
try {
|
|
1891
2478
|
ledger = Ledger.open(path);
|
|
@@ -1920,8 +2507,8 @@ async function main(argv) {
|
|
|
1920
2507
|
switch (command) {
|
|
1921
2508
|
case "check": {
|
|
1922
2509
|
const path = rest[0];
|
|
1923
|
-
if (path === void 0) fail("check needs a pack file");
|
|
1924
|
-
return check(path);
|
|
2510
|
+
if (path === void 0) fail("check needs a pack file or directory");
|
|
2511
|
+
return check(path, rest);
|
|
1925
2512
|
}
|
|
1926
2513
|
case "explain": {
|
|
1927
2514
|
const [path, tool, ...pairs] = rest;
|
|
@@ -1964,7 +2551,12 @@ async function main(argv) {
|
|
|
1964
2551
|
case "verify": {
|
|
1965
2552
|
const path = rest[0];
|
|
1966
2553
|
if (path === void 0) fail("verify needs a ledger file");
|
|
1967
|
-
return verify(path, flag(rest, "key"), flag(rest, "checkpoint"));
|
|
2554
|
+
return verify$1(path, flag(rest, "key"), flag(rest, "checkpoint"));
|
|
2555
|
+
}
|
|
2556
|
+
case "chain": {
|
|
2557
|
+
const path = rest[0];
|
|
2558
|
+
if (path === void 0) fail("chain needs a ledger file");
|
|
2559
|
+
return chainMatrix(path, rest);
|
|
1968
2560
|
}
|
|
1969
2561
|
case "checkpoint": {
|
|
1970
2562
|
const path = rest[0];
|
|
@@ -2007,7 +2599,26 @@ async function main(argv) {
|
|
|
2007
2599
|
...flag(rest, "out") === void 0 ? {} : { out: flag(rest, "out") }
|
|
2008
2600
|
});
|
|
2009
2601
|
}
|
|
2010
|
-
case "init":
|
|
2602
|
+
case "init":
|
|
2603
|
+
if (rest[0] === "claude-code") {
|
|
2604
|
+
const { initClaudeCode, stableNode } = await import("./init-DRd-IRR6.js");
|
|
2605
|
+
try {
|
|
2606
|
+
return initClaudeCode(rest.slice(1), {
|
|
2607
|
+
cwd: process.cwd(),
|
|
2608
|
+
node: stableNode(process.execPath, process.env.PATH),
|
|
2609
|
+
bin: resolve(process.argv[1] ?? ""),
|
|
2610
|
+
baseline: bundledBaseline()
|
|
2611
|
+
});
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
return init(rest);
|
|
2617
|
+
case "hook": {
|
|
2618
|
+
if (rest[0] !== "claude-code") fail("hook takes claude-code");
|
|
2619
|
+
const { runHook } = await import("./hook-Gll2spTP.js");
|
|
2620
|
+
return runHook(rest.slice(1));
|
|
2621
|
+
}
|
|
2011
2622
|
case "--help":
|
|
2012
2623
|
case "-h":
|
|
2013
2624
|
case void 0:
|