protect-mcp 0.6.2 → 0.7.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/CHANGELOG.md +47 -0
- package/README.md +11 -0
- package/dist/{chunk-UV53U6D4.mjs → chunk-546U3A7R.mjs} +79 -47
- package/dist/{chunk-PLKRTBDR.mjs → chunk-OHUTUFTC.mjs} +1 -1
- package/dist/{chunk-3YCKR72H.mjs → chunk-X63ELMU4.mjs} +1 -1
- package/dist/{chunk-S4ICHNSP.mjs → chunk-ZBKJANP7.mjs} +2 -2
- package/dist/cli.js +204 -53
- package/dist/cli.mjs +137 -12
- package/dist/hook-server.js +50 -47
- package/dist/hook-server.mjs +2 -2
- package/dist/{http-transport-MO32ESHZ.mjs → http-transport-R5AO7X6D.mjs} +2 -2
- package/dist/index.d.mts +38 -5
- package/dist/index.d.ts +38 -5
- package/dist/index.js +82 -48
- package/dist/index.mjs +8 -4
- package/package.json +4 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.7.0 (security release): the gate now fails closed and actually evaluates
|
|
4
|
+
|
|
5
|
+
This release fixes the way the Cedar policy gate behaves when anything goes
|
|
6
|
+
wrong, and a separate defect that meant Cedar policies were not being evaluated
|
|
7
|
+
at all against the pinned engine. If you rely on protect-mcp to enforce a Cedar
|
|
8
|
+
policy, upgrade.
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- **Fail closed, not open.** Before 0.7.0, if the Cedar engine was unavailable,
|
|
13
|
+
the result was malformed, evaluation threw, or a policy errored, the evaluator
|
|
14
|
+
returned ALLOW. A security gate must do the opposite. Every error and
|
|
15
|
+
uncertainty path now DENIES. The allow-on-error behavior is reachable only by
|
|
16
|
+
explicitly passing `{ failClosed: false }` (observe mode), and even then the
|
|
17
|
+
decision is flagged `would_deny: true` so a failure is never silent.
|
|
18
|
+
- **An errored policy can no longer permit-all.** Cedar silently discards a
|
|
19
|
+
policy that errors at evaluation (for example the `context.<string> in [list]`
|
|
20
|
+
type error in the 0.5.x and 0.6.x advisory), which could leave a residual
|
|
21
|
+
permit standing. The evaluator now treats any per-policy error as an
|
|
22
|
+
evaluation error and denies under enforcement.
|
|
23
|
+
- **Cedar policies are actually evaluated now.** The `isAuthorized` call passed
|
|
24
|
+
the policy text as a bare string, but `@cedar-policy/cedar-wasm@4.x` requires a
|
|
25
|
+
structured `PolicySet`. As a result every Cedar evaluation errored against the
|
|
26
|
+
pinned engine and (with the old fail-open default) allowed everything. The call
|
|
27
|
+
shape and the response parser are corrected, so a `forbid` rule actually denies
|
|
28
|
+
and a `permit` actually allows.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`protect-mcp evaluate`** and **`protect-mcp sign`**: one-shot per-call verbs
|
|
33
|
+
for PreToolUse and PostToolUse hooks. `evaluate` loads a Cedar policy, evaluates
|
|
34
|
+
one tool call fail-closed, and exits 2 on deny (so the tool is blocked) and 0 on
|
|
35
|
+
allow; a missing or unloadable policy denies unless `--fail-on-missing-policy
|
|
36
|
+
false` is set. This makes hook configs that invoke these verbs work as written.
|
|
37
|
+
- **`runEvaluatorSelfTest()`** plus a startup gate: `serve --enforce` runs the
|
|
38
|
+
self-test before arming and refuses to start if the engine cannot prove it
|
|
39
|
+
denies a known-forbidden vector. `protect-mcp doctor` reports the same, so the
|
|
40
|
+
gate verifies its own restraint before it is trusted.
|
|
41
|
+
- A CI tripwire test that fails the build if the discarded `in`-on-String pattern
|
|
42
|
+
is ever reintroduced into a shipped policy.
|
|
43
|
+
|
|
44
|
+
### Affected versions
|
|
45
|
+
|
|
46
|
+
The fail-open behavior and the unevaluated-Cedar defect are present in the
|
|
47
|
+
0.5.x and 0.6.x lines. Upgrade to 0.7.0.
|
package/README.md
CHANGED
|
@@ -401,6 +401,17 @@ npx @veritasacta/verify receipt.json --key <public-key-hex>
|
|
|
401
401
|
- **Patent Status**: 4 Australian provisional patents pending (2025-2026)
|
|
402
402
|
- **Cedar WASM**: [PR #64](https://github.com/cedar-policy/cedar-for-agents/pull/64) merged + [PR #73](https://github.com/cedar-policy/cedar-for-agents/pull/73) (RequestGenerator, pending review)
|
|
403
403
|
|
|
404
|
+
## What's New in v0.7.0 (security release)
|
|
405
|
+
|
|
406
|
+
The gate now fails closed. On any policy-evaluation error, a missing engine, or a
|
|
407
|
+
policy that errored, the decision is DENY, not allow. Cedar is also evaluated
|
|
408
|
+
correctly against cedar-wasm 4.x (earlier versions passed the policy in a shape the
|
|
409
|
+
engine rejected, so evaluation errored and the old fail-open default allowed
|
|
410
|
+
everything). `serve --enforce` now runs a restraint self-test before arming and
|
|
411
|
+
refuses to start if it cannot prove it denies a forbidden vector, `doctor` reports
|
|
412
|
+
that self-test, and one-shot `evaluate` and `sign` verbs are available for hooks.
|
|
413
|
+
Upgrade from any 0.5.x or 0.6.x. See CHANGELOG.md.
|
|
414
|
+
|
|
404
415
|
## What's New in v0.5.3
|
|
405
416
|
|
|
406
417
|
- `quickstart --connect`: Auto-create dashboard sandbox and configure receipt upload
|
|
@@ -289,14 +289,18 @@ function buildEntities(req) {
|
|
|
289
289
|
}
|
|
290
290
|
];
|
|
291
291
|
}
|
|
292
|
-
|
|
292
|
+
function onEvalError(reason, failClosed, extra) {
|
|
293
|
+
return {
|
|
294
|
+
allowed: !failClosed,
|
|
295
|
+
reason: failClosed ? reason : `${reason} (observe mode; would DENY under enforcement)`,
|
|
296
|
+
metadata: { error: true, fail_closed: failClosed, would_deny: true, ...extra || {} }
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async function evaluateCedar(policySet, req, schema, options) {
|
|
300
|
+
const failClosed = options?.failClosed ?? true;
|
|
293
301
|
const available = await ensureCedarWasm();
|
|
294
302
|
if (!available) {
|
|
295
|
-
return {
|
|
296
|
-
allowed: true,
|
|
297
|
-
reason: "cedar_wasm_not_available",
|
|
298
|
-
metadata: { fallback: true }
|
|
299
|
-
};
|
|
303
|
+
return onEvalError("cedar_wasm_not_available", failClosed, { fallback: true });
|
|
300
304
|
}
|
|
301
305
|
try {
|
|
302
306
|
const agentId = req.agentId || req.tier;
|
|
@@ -318,7 +322,7 @@ async function evaluateCedar(policySet, req, schema) {
|
|
|
318
322
|
let result;
|
|
319
323
|
if (typeof cedarWasm.isAuthorized === "function") {
|
|
320
324
|
result = cedarWasm.isAuthorized({
|
|
321
|
-
policies: policySet.source,
|
|
325
|
+
policies: { staticPolicies: policySet.source },
|
|
322
326
|
entities,
|
|
323
327
|
principal: authRequest.principal,
|
|
324
328
|
action: authRequest.action,
|
|
@@ -336,7 +340,7 @@ async function evaluateCedar(policySet, req, schema) {
|
|
|
336
340
|
const cedarEngine = cedarWasm.default || cedarWasm;
|
|
337
341
|
if (typeof cedarEngine.isAuthorized === "function") {
|
|
338
342
|
result = cedarEngine.isAuthorized({
|
|
339
|
-
policies: policySet.source,
|
|
343
|
+
policies: { staticPolicies: policySet.source },
|
|
340
344
|
entities,
|
|
341
345
|
principal: authRequest.principal,
|
|
342
346
|
action: authRequest.action,
|
|
@@ -345,61 +349,87 @@ async function evaluateCedar(policySet, req, schema) {
|
|
|
345
349
|
schema: cedarSchema
|
|
346
350
|
});
|
|
347
351
|
} else {
|
|
348
|
-
return {
|
|
349
|
-
allowed: true,
|
|
350
|
-
reason: "cedar_wasm_api_unsupported",
|
|
351
|
-
metadata: { fallback: true, exports: Object.keys(cedarWasm) }
|
|
352
|
-
};
|
|
352
|
+
return onEvalError("cedar_wasm_api_unsupported", failClosed, { exports: Object.keys(cedarWasm) });
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
-
const
|
|
355
|
+
const parsed = parseWasmResult(result);
|
|
356
|
+
const policyErrors = extractPolicyErrors(result);
|
|
357
|
+
if (parsed.kind === "error") {
|
|
358
|
+
return onEvalError(`cedar_unparseable_result: ${parsed.diagnostics}`, failClosed);
|
|
359
|
+
}
|
|
360
|
+
if (policyErrors.length > 0) {
|
|
361
|
+
return onEvalError(
|
|
362
|
+
`cedar_policy_errored: ${policyErrors.length} policy error(s); decision is unsound`,
|
|
363
|
+
failClosed,
|
|
364
|
+
{ policy_errors: policyErrors.slice(0, 5), policy_digest: policySet.digest }
|
|
365
|
+
);
|
|
366
|
+
}
|
|
356
367
|
return {
|
|
357
|
-
allowed:
|
|
358
|
-
reason:
|
|
368
|
+
allowed: parsed.kind === "allow",
|
|
369
|
+
reason: parsed.kind === "allow" ? void 0 : `cedar_deny${parsed.diagnostics ? ": " + parsed.diagnostics : ""}`,
|
|
359
370
|
metadata: {
|
|
360
371
|
policy_digest: policySet.digest,
|
|
361
|
-
...
|
|
372
|
+
...parsed.matchedPolicies ? { matched_policies: parsed.matchedPolicies } : {}
|
|
362
373
|
}
|
|
363
374
|
};
|
|
364
375
|
} catch (err) {
|
|
365
|
-
return {
|
|
366
|
-
allowed: true,
|
|
367
|
-
reason: `cedar_eval_error: ${err instanceof Error ? err.message : "unknown"}`,
|
|
368
|
-
metadata: { fallback: true, error: true }
|
|
369
|
-
};
|
|
376
|
+
return onEvalError(`cedar_eval_error: ${err instanceof Error ? err.message : "unknown"}`, failClosed);
|
|
370
377
|
}
|
|
371
378
|
}
|
|
372
379
|
function parseWasmResult(result) {
|
|
373
|
-
if (!result) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
|
|
382
|
-
diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0,
|
|
383
|
-
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
if (result.decision === "Allow") {
|
|
387
|
-
return { allowed: true };
|
|
388
|
-
}
|
|
389
|
-
if (result.decision === "Deny") {
|
|
390
|
-
return {
|
|
391
|
-
allowed: false,
|
|
392
|
-
diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
if (typeof result === "boolean") {
|
|
396
|
-
return { allowed: result };
|
|
380
|
+
if (!result) return { kind: "error", diagnostics: "null result from Cedar WASM" };
|
|
381
|
+
if (result.type === "failure") {
|
|
382
|
+
return { kind: "error", diagnostics: `cedar failure: ${JSON.stringify(result.errors ?? [])}` };
|
|
383
|
+
}
|
|
384
|
+
if (result.type === "success" && result.response) {
|
|
385
|
+
const dec = result.response.decision;
|
|
386
|
+
const reasons = result.response.diagnostics?.reason;
|
|
387
|
+
if (dec === "allow" || dec === "Allow") return { kind: "allow", matchedPolicies: reasons };
|
|
388
|
+
if (dec === "deny" || dec === "Deny") {
|
|
389
|
+
return { kind: "deny", diagnostics: result.response.diagnostics ? JSON.stringify(result.response.diagnostics) : void 0, matchedPolicies: reasons };
|
|
390
|
+
}
|
|
397
391
|
}
|
|
398
|
-
|
|
392
|
+
if (result.type === "allow" || result.decision === "Allow") return { kind: "allow" };
|
|
393
|
+
if (result.type === "deny" || result.decision === "Deny") return { kind: "deny" };
|
|
394
|
+
if (typeof result === "boolean") return result ? { kind: "allow" } : { kind: "deny" };
|
|
395
|
+
return { kind: "error", diagnostics: `unknown result format: ${JSON.stringify(result)}` };
|
|
396
|
+
}
|
|
397
|
+
function extractPolicyErrors(result) {
|
|
398
|
+
if (!result || typeof result !== "object") return [];
|
|
399
|
+
const raw = result.errors ?? result.response?.diagnostics?.errors ?? result.diagnostics?.errors ?? [];
|
|
400
|
+
if (!Array.isArray(raw)) return [];
|
|
401
|
+
return raw.map((e) => typeof e === "string" ? e : e?.message ?? e?.error ?? JSON.stringify(e)).filter(Boolean);
|
|
399
402
|
}
|
|
400
403
|
async function isCedarAvailable() {
|
|
401
404
|
return ensureCedarWasm();
|
|
402
405
|
}
|
|
406
|
+
function policySetFromSource(source, name = "inline") {
|
|
407
|
+
const digest = createHash2("sha256").update(source).digest("hex").slice(0, 16);
|
|
408
|
+
return { source, digest, fileCount: 1, files: [name] };
|
|
409
|
+
}
|
|
410
|
+
async function runEvaluatorSelfTest() {
|
|
411
|
+
const wasmAvailable = await isCedarAvailable();
|
|
412
|
+
const cases = [];
|
|
413
|
+
const run = async (name, expected, policy, context) => {
|
|
414
|
+
const d = await evaluateCedar(policy, { tool: "Bash", tier: "unknown", context }, void 0, { failClosed: true });
|
|
415
|
+
const actual = d.allowed ? "ALLOW" : "DENY";
|
|
416
|
+
cases.push({ name, expected, actual, pass: actual === expected, reason: d.reason });
|
|
417
|
+
};
|
|
418
|
+
if (!wasmAvailable) {
|
|
419
|
+
await run("engine unavailable denies", "DENY", policySetFromSource("permit(principal, action, resource);"), {});
|
|
420
|
+
return { wasmAvailable, passed: cases.every((c) => c.pass), cases };
|
|
421
|
+
}
|
|
422
|
+
const correct = policySetFromSource(
|
|
423
|
+
'forbid(principal, action, resource) when { ["rm", "dd", "mkfs"].contains(context.command) };\npermit(principal, action, resource);'
|
|
424
|
+
);
|
|
425
|
+
await run("forbid denies rm", "DENY", correct, { command: "rm" });
|
|
426
|
+
await run("permit allows ls", "ALLOW", correct, { command: "ls" });
|
|
427
|
+
const broken = policySetFromSource(
|
|
428
|
+
'forbid(principal, action, resource) when { context.command in ["rm", "dd"] };\npermit(principal, action, resource);'
|
|
429
|
+
);
|
|
430
|
+
await run("in-on-String forbid does not permit-all", "DENY", broken, { command: "rm" });
|
|
431
|
+
return { wasmAvailable, passed: cases.every((c) => c.pass), cases };
|
|
432
|
+
}
|
|
403
433
|
|
|
404
434
|
// src/http-server.ts
|
|
405
435
|
import { createServer } from "http";
|
|
@@ -638,6 +668,8 @@ export {
|
|
|
638
668
|
loadCedarPolicies,
|
|
639
669
|
evaluateCedar,
|
|
640
670
|
isCedarAvailable,
|
|
671
|
+
policySetFromSource,
|
|
672
|
+
runEvaluatorSelfTest,
|
|
641
673
|
ReceiptBuffer,
|
|
642
674
|
startStatusServer
|
|
643
675
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
meetsMinTier
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-OHUTUFTC.mjs";
|
|
4
4
|
import {
|
|
5
5
|
checkRateLimit,
|
|
6
6
|
getToolPolicy,
|
|
7
7
|
parseRateLimit
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-546U3A7R.mjs";
|
|
9
9
|
|
|
10
10
|
// src/simulate.ts
|
|
11
11
|
import { readFileSync } from "fs";
|
package/dist/cli.js
CHANGED
|
@@ -739,14 +739,18 @@ function buildEntities(req) {
|
|
|
739
739
|
}
|
|
740
740
|
];
|
|
741
741
|
}
|
|
742
|
-
|
|
742
|
+
function onEvalError(reason, failClosed, extra) {
|
|
743
|
+
return {
|
|
744
|
+
allowed: !failClosed,
|
|
745
|
+
reason: failClosed ? reason : `${reason} (observe mode; would DENY under enforcement)`,
|
|
746
|
+
metadata: { error: true, fail_closed: failClosed, would_deny: true, ...extra || {} }
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
async function evaluateCedar(policySet, req, schema, options) {
|
|
750
|
+
const failClosed = options?.failClosed ?? true;
|
|
743
751
|
const available = await ensureCedarWasm();
|
|
744
752
|
if (!available) {
|
|
745
|
-
return {
|
|
746
|
-
allowed: true,
|
|
747
|
-
reason: "cedar_wasm_not_available",
|
|
748
|
-
metadata: { fallback: true }
|
|
749
|
-
};
|
|
753
|
+
return onEvalError("cedar_wasm_not_available", failClosed, { fallback: true });
|
|
750
754
|
}
|
|
751
755
|
try {
|
|
752
756
|
const agentId = req.agentId || req.tier;
|
|
@@ -768,7 +772,7 @@ async function evaluateCedar(policySet, req, schema) {
|
|
|
768
772
|
let result;
|
|
769
773
|
if (typeof cedarWasm.isAuthorized === "function") {
|
|
770
774
|
result = cedarWasm.isAuthorized({
|
|
771
|
-
policies: policySet.source,
|
|
775
|
+
policies: { staticPolicies: policySet.source },
|
|
772
776
|
entities,
|
|
773
777
|
principal: authRequest.principal,
|
|
774
778
|
action: authRequest.action,
|
|
@@ -786,7 +790,7 @@ async function evaluateCedar(policySet, req, schema) {
|
|
|
786
790
|
const cedarEngine = cedarWasm.default || cedarWasm;
|
|
787
791
|
if (typeof cedarEngine.isAuthorized === "function") {
|
|
788
792
|
result = cedarEngine.isAuthorized({
|
|
789
|
-
policies: policySet.source,
|
|
793
|
+
policies: { staticPolicies: policySet.source },
|
|
790
794
|
entities,
|
|
791
795
|
principal: authRequest.principal,
|
|
792
796
|
action: authRequest.action,
|
|
@@ -795,61 +799,87 @@ async function evaluateCedar(policySet, req, schema) {
|
|
|
795
799
|
schema: cedarSchema
|
|
796
800
|
});
|
|
797
801
|
} else {
|
|
798
|
-
return {
|
|
799
|
-
allowed: true,
|
|
800
|
-
reason: "cedar_wasm_api_unsupported",
|
|
801
|
-
metadata: { fallback: true, exports: Object.keys(cedarWasm) }
|
|
802
|
-
};
|
|
802
|
+
return onEvalError("cedar_wasm_api_unsupported", failClosed, { exports: Object.keys(cedarWasm) });
|
|
803
803
|
}
|
|
804
804
|
}
|
|
805
|
-
const
|
|
805
|
+
const parsed = parseWasmResult(result);
|
|
806
|
+
const policyErrors = extractPolicyErrors(result);
|
|
807
|
+
if (parsed.kind === "error") {
|
|
808
|
+
return onEvalError(`cedar_unparseable_result: ${parsed.diagnostics}`, failClosed);
|
|
809
|
+
}
|
|
810
|
+
if (policyErrors.length > 0) {
|
|
811
|
+
return onEvalError(
|
|
812
|
+
`cedar_policy_errored: ${policyErrors.length} policy error(s); decision is unsound`,
|
|
813
|
+
failClosed,
|
|
814
|
+
{ policy_errors: policyErrors.slice(0, 5), policy_digest: policySet.digest }
|
|
815
|
+
);
|
|
816
|
+
}
|
|
806
817
|
return {
|
|
807
|
-
allowed:
|
|
808
|
-
reason:
|
|
818
|
+
allowed: parsed.kind === "allow",
|
|
819
|
+
reason: parsed.kind === "allow" ? void 0 : `cedar_deny${parsed.diagnostics ? ": " + parsed.diagnostics : ""}`,
|
|
809
820
|
metadata: {
|
|
810
821
|
policy_digest: policySet.digest,
|
|
811
|
-
...
|
|
822
|
+
...parsed.matchedPolicies ? { matched_policies: parsed.matchedPolicies } : {}
|
|
812
823
|
}
|
|
813
824
|
};
|
|
814
825
|
} catch (err) {
|
|
815
|
-
return {
|
|
816
|
-
allowed: true,
|
|
817
|
-
reason: `cedar_eval_error: ${err instanceof Error ? err.message : "unknown"}`,
|
|
818
|
-
metadata: { fallback: true, error: true }
|
|
819
|
-
};
|
|
826
|
+
return onEvalError(`cedar_eval_error: ${err instanceof Error ? err.message : "unknown"}`, failClosed);
|
|
820
827
|
}
|
|
821
828
|
}
|
|
822
829
|
function parseWasmResult(result) {
|
|
823
|
-
if (!result) {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if (result.type === "allow" || result.type === "Allow") {
|
|
827
|
-
return { allowed: true };
|
|
828
|
-
}
|
|
829
|
-
if (result.type === "deny" || result.type === "Deny") {
|
|
830
|
-
return {
|
|
831
|
-
allowed: false,
|
|
832
|
-
diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0,
|
|
833
|
-
matchedPolicies: result.diagnostics?.reasons
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
if (result.decision === "Allow") {
|
|
837
|
-
return { allowed: true };
|
|
838
|
-
}
|
|
839
|
-
if (result.decision === "Deny") {
|
|
840
|
-
return {
|
|
841
|
-
allowed: false,
|
|
842
|
-
diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0
|
|
843
|
-
};
|
|
830
|
+
if (!result) return { kind: "error", diagnostics: "null result from Cedar WASM" };
|
|
831
|
+
if (result.type === "failure") {
|
|
832
|
+
return { kind: "error", diagnostics: `cedar failure: ${JSON.stringify(result.errors ?? [])}` };
|
|
844
833
|
}
|
|
845
|
-
if (
|
|
846
|
-
|
|
834
|
+
if (result.type === "success" && result.response) {
|
|
835
|
+
const dec = result.response.decision;
|
|
836
|
+
const reasons = result.response.diagnostics?.reason;
|
|
837
|
+
if (dec === "allow" || dec === "Allow") return { kind: "allow", matchedPolicies: reasons };
|
|
838
|
+
if (dec === "deny" || dec === "Deny") {
|
|
839
|
+
return { kind: "deny", diagnostics: result.response.diagnostics ? JSON.stringify(result.response.diagnostics) : void 0, matchedPolicies: reasons };
|
|
840
|
+
}
|
|
847
841
|
}
|
|
848
|
-
|
|
842
|
+
if (result.type === "allow" || result.decision === "Allow") return { kind: "allow" };
|
|
843
|
+
if (result.type === "deny" || result.decision === "Deny") return { kind: "deny" };
|
|
844
|
+
if (typeof result === "boolean") return result ? { kind: "allow" } : { kind: "deny" };
|
|
845
|
+
return { kind: "error", diagnostics: `unknown result format: ${JSON.stringify(result)}` };
|
|
846
|
+
}
|
|
847
|
+
function extractPolicyErrors(result) {
|
|
848
|
+
if (!result || typeof result !== "object") return [];
|
|
849
|
+
const raw = result.errors ?? result.response?.diagnostics?.errors ?? result.diagnostics?.errors ?? [];
|
|
850
|
+
if (!Array.isArray(raw)) return [];
|
|
851
|
+
return raw.map((e) => typeof e === "string" ? e : e?.message ?? e?.error ?? JSON.stringify(e)).filter(Boolean);
|
|
849
852
|
}
|
|
850
853
|
async function isCedarAvailable() {
|
|
851
854
|
return ensureCedarWasm();
|
|
852
855
|
}
|
|
856
|
+
function policySetFromSource(source, name = "inline") {
|
|
857
|
+
const digest = (0, import_node_crypto2.createHash)("sha256").update(source).digest("hex").slice(0, 16);
|
|
858
|
+
return { source, digest, fileCount: 1, files: [name] };
|
|
859
|
+
}
|
|
860
|
+
async function runEvaluatorSelfTest() {
|
|
861
|
+
const wasmAvailable = await isCedarAvailable();
|
|
862
|
+
const cases = [];
|
|
863
|
+
const run = async (name, expected, policy, context) => {
|
|
864
|
+
const d = await evaluateCedar(policy, { tool: "Bash", tier: "unknown", context }, void 0, { failClosed: true });
|
|
865
|
+
const actual = d.allowed ? "ALLOW" : "DENY";
|
|
866
|
+
cases.push({ name, expected, actual, pass: actual === expected, reason: d.reason });
|
|
867
|
+
};
|
|
868
|
+
if (!wasmAvailable) {
|
|
869
|
+
await run("engine unavailable denies", "DENY", policySetFromSource("permit(principal, action, resource);"), {});
|
|
870
|
+
return { wasmAvailable, passed: cases.every((c) => c.pass), cases };
|
|
871
|
+
}
|
|
872
|
+
const correct = policySetFromSource(
|
|
873
|
+
'forbid(principal, action, resource) when { ["rm", "dd", "mkfs"].contains(context.command) };\npermit(principal, action, resource);'
|
|
874
|
+
);
|
|
875
|
+
await run("forbid denies rm", "DENY", correct, { command: "rm" });
|
|
876
|
+
await run("permit allows ls", "ALLOW", correct, { command: "ls" });
|
|
877
|
+
const broken = policySetFromSource(
|
|
878
|
+
'forbid(principal, action, resource) when { context.command in ["rm", "dd"] };\npermit(principal, action, resource);'
|
|
879
|
+
);
|
|
880
|
+
await run("in-on-String forbid does not permit-all", "DENY", broken, { command: "rm" });
|
|
881
|
+
return { wasmAvailable, passed: cases.every((c) => c.pass), cases };
|
|
882
|
+
}
|
|
853
883
|
var import_node_crypto2, import_node_fs4, import_node_path2, cedarWasm, loadAttempted;
|
|
854
884
|
var init_cedar_evaluator = __esm({
|
|
855
885
|
"src/cedar-evaluator.ts"() {
|
|
@@ -6315,6 +6345,8 @@ function formatSimulation(summary) {
|
|
|
6315
6345
|
|
|
6316
6346
|
// src/cli.ts
|
|
6317
6347
|
init_cedar_evaluator();
|
|
6348
|
+
var import_node_fs10 = require("fs");
|
|
6349
|
+
var import_node_path6 = require("path");
|
|
6318
6350
|
var import_meta = {};
|
|
6319
6351
|
function printHelp() {
|
|
6320
6352
|
process.stderr.write(`
|
|
@@ -6348,6 +6380,8 @@ Options:
|
|
|
6348
6380
|
|
|
6349
6381
|
Commands:
|
|
6350
6382
|
serve Start HTTP hook server for Claude Code integration (port 9377)
|
|
6383
|
+
evaluate Evaluate one tool call against a Cedar policy (PreToolUse gate; exit 2 = deny, fail-closed)
|
|
6384
|
+
sign Sign one tool call into a receipt (PostToolUse)
|
|
6351
6385
|
init-hooks Generate Claude Code hook config + skill + sample Cedar policy
|
|
6352
6386
|
quickstart Zero-config onboarding: init + demo + show receipts in one command
|
|
6353
6387
|
connect Create a ScopeBlind sandbox dashboard and configure receipt upload
|
|
@@ -7572,6 +7606,88 @@ async function sendInstallTelemetry() {
|
|
|
7572
7606
|
} catch {
|
|
7573
7607
|
}
|
|
7574
7608
|
}
|
|
7609
|
+
function flagValue(argv, name) {
|
|
7610
|
+
const i = argv.indexOf(name);
|
|
7611
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : void 0;
|
|
7612
|
+
}
|
|
7613
|
+
function loadPolicyArg(argv) {
|
|
7614
|
+
const cedarDir = flagValue(argv, "--cedar");
|
|
7615
|
+
const policyFile = flagValue(argv, "--policy");
|
|
7616
|
+
try {
|
|
7617
|
+
if (cedarDir) return loadCedarPolicies(cedarDir);
|
|
7618
|
+
if (policyFile && (0, import_node_fs10.existsSync)(policyFile)) {
|
|
7619
|
+
return policySetFromSource((0, import_node_fs10.readFileSync)(policyFile, "utf-8"), (0, import_node_path6.basename)(policyFile));
|
|
7620
|
+
}
|
|
7621
|
+
} catch {
|
|
7622
|
+
}
|
|
7623
|
+
return null;
|
|
7624
|
+
}
|
|
7625
|
+
async function handleEvaluate(argv) {
|
|
7626
|
+
const tool = flagValue(argv, "--tool") || "";
|
|
7627
|
+
const inputRaw = flagValue(argv, "--input") || "{}";
|
|
7628
|
+
const contextRaw = flagValue(argv, "--context");
|
|
7629
|
+
const failOnMissing = flagValue(argv, "--fail-on-missing-policy") !== "false";
|
|
7630
|
+
const policySet = loadPolicyArg(argv);
|
|
7631
|
+
if (!policySet) {
|
|
7632
|
+
if (failOnMissing) {
|
|
7633
|
+
process.stderr.write("protect-mcp evaluate: policy not found; denying (fail-closed). Pass --fail-on-missing-policy false to allow.\n");
|
|
7634
|
+
process.exit(2);
|
|
7635
|
+
}
|
|
7636
|
+
process.stdout.write(JSON.stringify({ allowed: true, reason: "no_policy_configured" }) + "\n");
|
|
7637
|
+
process.exit(0);
|
|
7638
|
+
}
|
|
7639
|
+
let input = {};
|
|
7640
|
+
try {
|
|
7641
|
+
input = JSON.parse(inputRaw);
|
|
7642
|
+
} catch {
|
|
7643
|
+
}
|
|
7644
|
+
let extra = {};
|
|
7645
|
+
if (contextRaw) {
|
|
7646
|
+
try {
|
|
7647
|
+
extra = JSON.parse(contextRaw);
|
|
7648
|
+
} catch {
|
|
7649
|
+
}
|
|
7650
|
+
}
|
|
7651
|
+
const context = { ...input, ...extra };
|
|
7652
|
+
if (typeof input.command === "string" && context.command_pattern === void 0) {
|
|
7653
|
+
context.command_pattern = input.command;
|
|
7654
|
+
}
|
|
7655
|
+
const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context }, void 0, { failClosed: true });
|
|
7656
|
+
process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
|
|
7657
|
+
process.exit(decision.allowed ? 0 : 2);
|
|
7658
|
+
}
|
|
7659
|
+
async function handleSign(argv) {
|
|
7660
|
+
const tool = flagValue(argv, "--tool") || "";
|
|
7661
|
+
const receiptsDir = flagValue(argv, "--receipts") || "./receipts/";
|
|
7662
|
+
const keyPath = flagValue(argv, "--key");
|
|
7663
|
+
if (keyPath && (0, import_node_fs10.existsSync)(keyPath)) {
|
|
7664
|
+
try {
|
|
7665
|
+
await initSigning({ enabled: true, key_path: keyPath });
|
|
7666
|
+
} catch {
|
|
7667
|
+
}
|
|
7668
|
+
}
|
|
7669
|
+
const requestId = `tu-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
7670
|
+
const signed = signDecision({
|
|
7671
|
+
tool,
|
|
7672
|
+
decision: "allow",
|
|
7673
|
+
reason_code: "post_execution_receipt",
|
|
7674
|
+
policy_digest: "none",
|
|
7675
|
+
request_id: requestId,
|
|
7676
|
+
mode: "enforce",
|
|
7677
|
+
timestamp: Date.now()
|
|
7678
|
+
});
|
|
7679
|
+
try {
|
|
7680
|
+
(0, import_node_fs10.mkdirSync)(receiptsDir, { recursive: true });
|
|
7681
|
+
} catch {
|
|
7682
|
+
}
|
|
7683
|
+
const line = signed.signed ?? JSON.stringify({ tool, request_id: requestId, signed: false, note: signed.warning || "no signer configured" });
|
|
7684
|
+
try {
|
|
7685
|
+
(0, import_node_fs10.appendFileSync)((0, import_node_path6.join)(receiptsDir, "receipts.jsonl"), line + "\n");
|
|
7686
|
+
} catch {
|
|
7687
|
+
}
|
|
7688
|
+
process.stdout.write(JSON.stringify({ signed: Boolean(signed.signed), artifact_type: signed.artifact_type, request_id: requestId }) + "\n");
|
|
7689
|
+
process.exit(0);
|
|
7690
|
+
}
|
|
7575
7691
|
async function main() {
|
|
7576
7692
|
sendInstallTelemetry().catch(() => {
|
|
7577
7693
|
});
|
|
@@ -7580,6 +7696,14 @@ async function main() {
|
|
|
7580
7696
|
printHelp();
|
|
7581
7697
|
process.exit(0);
|
|
7582
7698
|
}
|
|
7699
|
+
if (args[0] === "evaluate") {
|
|
7700
|
+
await handleEvaluate(args.slice(1));
|
|
7701
|
+
return;
|
|
7702
|
+
}
|
|
7703
|
+
if (args[0] === "sign") {
|
|
7704
|
+
await handleSign(args.slice(1));
|
|
7705
|
+
return;
|
|
7706
|
+
}
|
|
7583
7707
|
if (args[0] === "serve") {
|
|
7584
7708
|
const { startHookServer: startHookServer2 } = await Promise.resolve().then(() => (init_hook_server(), hook_server_exports));
|
|
7585
7709
|
const portIdx = args.indexOf("--port");
|
|
@@ -7588,13 +7712,22 @@ async function main() {
|
|
|
7588
7712
|
const policyPath2 = policyIdx >= 0 && args[policyIdx + 1] ? args[policyIdx + 1] : void 0;
|
|
7589
7713
|
const cedarIdx = args.indexOf("--cedar");
|
|
7590
7714
|
const cedarDir2 = cedarIdx >= 0 && args[cedarIdx + 1] ? args[cedarIdx + 1] : void 0;
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7715
|
+
const enforce2 = args.includes("--enforce");
|
|
7716
|
+
const verbose2 = args.includes("--verbose") || args.includes("-v");
|
|
7717
|
+
if (enforce2) {
|
|
7718
|
+
const selfTest = await runEvaluatorSelfTest();
|
|
7719
|
+
if (!selfTest.passed) {
|
|
7720
|
+
process.stderr.write("protect-mcp serve --enforce: the policy-engine restraint self-test FAILED. Refusing to arm the gate.\n");
|
|
7721
|
+
for (const c of selfTest.cases.filter((c2) => !c2.pass)) {
|
|
7722
|
+
process.stderr.write(` [FAIL] ${c.name}: expected ${c.expected}, got ${c.actual}
|
|
7723
|
+
`);
|
|
7724
|
+
}
|
|
7725
|
+
process.exit(1);
|
|
7726
|
+
}
|
|
7727
|
+
if (verbose2) process.stderr.write(`protect-mcp: restraint self-test passed (${selfTest.cases.length} vectors). Arming gate.
|
|
7728
|
+
`);
|
|
7729
|
+
}
|
|
7730
|
+
await startHookServer2({ port, policyPath: policyPath2, cedarDir: cedarDir2, enforce: enforce2, verbose: verbose2 });
|
|
7598
7731
|
return;
|
|
7599
7732
|
}
|
|
7600
7733
|
if (args[0] === "init-hooks") {
|
|
@@ -7904,6 +8037,24 @@ async function handleDoctor() {
|
|
|
7904
8037
|
} catch {
|
|
7905
8038
|
process.stdout.write(dim2(" ScopeBlind API not reachable \u2014 offline mode (receipts stored locally)\n"));
|
|
7906
8039
|
}
|
|
8040
|
+
process.stdout.write("\nRestraint self-test:\n");
|
|
8041
|
+
try {
|
|
8042
|
+
const st = await runEvaluatorSelfTest();
|
|
8043
|
+
if (!st.wasmAvailable) {
|
|
8044
|
+
process.stdout.write(dim2(" Cedar WASM not installed; the gate fails closed (denies) until it is.\n"));
|
|
8045
|
+
}
|
|
8046
|
+
for (const c of st.cases) {
|
|
8047
|
+
process.stdout.write(c.pass ? green2(` ${c.name}
|
|
8048
|
+
`) : `\x1B[31m FAIL: ${c.name} (expected ${c.expected}, got ${c.actual})
|
|
8049
|
+
\x1B[0m`);
|
|
8050
|
+
}
|
|
8051
|
+
if (!st.passed) issues++;
|
|
8052
|
+
else process.stdout.write(green2(" the gate denies what it should and allows what it should\n"));
|
|
8053
|
+
} catch (err) {
|
|
8054
|
+
process.stdout.write(yellow2(` self-test could not run: ${err instanceof Error ? err.message : "unknown"}
|
|
8055
|
+
`));
|
|
8056
|
+
issues++;
|
|
8057
|
+
}
|
|
7907
8058
|
process.stdout.write("\n");
|
|
7908
8059
|
if (issues === 0) {
|
|
7909
8060
|
process.stdout.write("\x1B[32m\x1B[1mAll checks passed.\x1B[0m Ready to wrap MCP servers.\n");
|