protect-mcp 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/bundle-TXOTFJIJ.mjs +8 -0
- package/dist/chunk-5JXFV37Y.mjs +53 -0
- package/dist/{chunk-3WCA7O4D.mjs → chunk-GV7N53QE.mjs} +300 -11
- package/dist/chunk-JQDVKZBN.mjs +165 -0
- package/dist/cli.js +1180 -38
- package/dist/cli.mjs +640 -11
- package/dist/index.d.mts +156 -2
- package/dist/index.d.ts +156 -2
- package/dist/index.js +466 -19
- package/dist/index.mjs +17 -50
- package/dist/report-ENQ3KUI2.mjs +8 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
@@ -36,6 +26,9 @@ __export(index_exports, {
|
|
|
36
26
|
collectSignedReceipts: () => collectSignedReceipts,
|
|
37
27
|
createAuditBundle: () => createAuditBundle,
|
|
38
28
|
evaluateTier: () => evaluateTier,
|
|
29
|
+
formatReportMarkdown: () => formatReportMarkdown,
|
|
30
|
+
formatSimulation: () => formatSimulation,
|
|
31
|
+
generateReport: () => generateReport,
|
|
39
32
|
getSignerInfo: () => getSignerInfo,
|
|
40
33
|
getToolPolicy: () => getToolPolicy,
|
|
41
34
|
initSigning: () => initSigning,
|
|
@@ -47,10 +40,12 @@ __export(index_exports, {
|
|
|
47
40
|
listCredentialLabels: () => listCredentialLabels,
|
|
48
41
|
loadPolicy: () => loadPolicy,
|
|
49
42
|
meetsMinTier: () => meetsMinTier,
|
|
43
|
+
parseLogFile: () => parseLogFile,
|
|
50
44
|
parseRateLimit: () => parseRateLimit,
|
|
51
45
|
queryExternalPDP: () => queryExternalPDP,
|
|
52
46
|
resolveCredential: () => resolveCredential,
|
|
53
47
|
signDecision: () => signDecision,
|
|
48
|
+
simulate: () => simulate,
|
|
54
49
|
validateCredentials: () => validateCredentials,
|
|
55
50
|
validateEvidenceReceipt: () => validateEvidenceReceipt,
|
|
56
51
|
validateManifest: () => validateManifest
|
|
@@ -383,7 +378,11 @@ async function initSigning(config) {
|
|
|
383
378
|
return warnings;
|
|
384
379
|
}
|
|
385
380
|
try {
|
|
386
|
-
|
|
381
|
+
const moduleName = "@veritasacta/artifacts";
|
|
382
|
+
artifactsModule = await import(
|
|
383
|
+
/* @vite-ignore */
|
|
384
|
+
moduleName
|
|
385
|
+
);
|
|
387
386
|
} catch {
|
|
388
387
|
warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
|
|
389
388
|
return warnings;
|
|
@@ -402,8 +401,8 @@ async function initSigning(config) {
|
|
|
402
401
|
signerState = {
|
|
403
402
|
privateKey: keyData.privateKey,
|
|
404
403
|
publicKey: keyData.publicKey,
|
|
405
|
-
kid: artifactsModule.computeKid(keyData.publicKey),
|
|
406
|
-
issuer: config.issuer || "protect-mcp"
|
|
404
|
+
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
405
|
+
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
407
406
|
};
|
|
408
407
|
} catch (err) {
|
|
409
408
|
warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
|
|
@@ -615,13 +614,16 @@ var ReceiptBuffer = class {
|
|
|
615
614
|
count() {
|
|
616
615
|
return this.receipts.length;
|
|
617
616
|
}
|
|
617
|
+
getLatest() {
|
|
618
|
+
return this.receipts.length > 0 ? this.receipts[this.receipts.length - 1] : void 0;
|
|
619
|
+
}
|
|
618
620
|
};
|
|
619
|
-
function startStatusServer(config, receiptBuffer) {
|
|
621
|
+
function startStatusServer(config, receiptBuffer, approvalStore, approvalNonce) {
|
|
620
622
|
const startTime = Date.now();
|
|
621
623
|
const logDir = process.cwd();
|
|
622
624
|
const server = (0, import_node_http.createServer)((req, res) => {
|
|
623
625
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
624
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
626
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
625
627
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
626
628
|
res.setHeader("Content-Type", "application/json");
|
|
627
629
|
if (req.method === "OPTIONS") {
|
|
@@ -638,18 +640,30 @@ function startStatusServer(config, receiptBuffer) {
|
|
|
638
640
|
handleStatus(res, logDir);
|
|
639
641
|
} else if (path === "/receipts") {
|
|
640
642
|
handleReceipts(res, receiptBuffer, url);
|
|
643
|
+
} else if (path === "/receipts/latest") {
|
|
644
|
+
handleReceiptLatest(res, receiptBuffer);
|
|
641
645
|
} else if (path.startsWith("/receipts/")) {
|
|
642
646
|
const id = path.slice("/receipts/".length);
|
|
643
647
|
handleReceiptById(res, receiptBuffer, id);
|
|
648
|
+
} else if (path === "/approve" && req.method === "POST") {
|
|
649
|
+
handleApprove(req, res, approvalStore, approvalNonce);
|
|
650
|
+
} else if (path === "/approvals" && req.method === "GET") {
|
|
651
|
+
handleListApprovals(res, approvalStore);
|
|
644
652
|
} else {
|
|
645
653
|
res.writeHead(404);
|
|
646
|
-
res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/:id"] }));
|
|
654
|
+
res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/latest", "/receipts/:id", "/approve", "/approvals"] }));
|
|
647
655
|
}
|
|
648
656
|
} catch (err) {
|
|
649
657
|
res.writeHead(500);
|
|
650
658
|
res.end(JSON.stringify({ error: "internal_error" }));
|
|
651
659
|
}
|
|
652
660
|
});
|
|
661
|
+
server.on("error", (err) => {
|
|
662
|
+
if (config.verbose) {
|
|
663
|
+
process.stderr.write(`[PROTECT_MCP] HTTP status server error: ${err.message}
|
|
664
|
+
`);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
653
667
|
server.listen(config.port, "127.0.0.1", () => {
|
|
654
668
|
if (config.verbose) {
|
|
655
669
|
process.stderr.write(`[PROTECT_MCP] HTTP status server listening on http://127.0.0.1:${config.port}
|
|
@@ -665,7 +679,7 @@ function handleHealth(res, startTime, config) {
|
|
|
665
679
|
status: "ok",
|
|
666
680
|
uptime_ms: Date.now() - startTime,
|
|
667
681
|
mode: config.mode,
|
|
668
|
-
version: "0.3.
|
|
682
|
+
version: "0.3.1"
|
|
669
683
|
}));
|
|
670
684
|
}
|
|
671
685
|
function handleStatus(res, logDir) {
|
|
@@ -714,6 +728,16 @@ function handleReceipts(res, buffer, url) {
|
|
|
714
728
|
receipts
|
|
715
729
|
}));
|
|
716
730
|
}
|
|
731
|
+
function handleReceiptLatest(res, buffer) {
|
|
732
|
+
const latest = buffer.getLatest();
|
|
733
|
+
if (!latest) {
|
|
734
|
+
res.writeHead(404);
|
|
735
|
+
res.end(JSON.stringify({ error: "no_receipts", message: "No receipts yet. Make a tool call through protect-mcp first." }));
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
res.writeHead(200);
|
|
739
|
+
res.end(JSON.stringify(latest));
|
|
740
|
+
}
|
|
717
741
|
function handleReceiptById(res, buffer, id) {
|
|
718
742
|
const receipt = buffer.getById(id);
|
|
719
743
|
if (!receipt) {
|
|
@@ -724,22 +748,92 @@ function handleReceiptById(res, buffer, id) {
|
|
|
724
748
|
res.writeHead(200);
|
|
725
749
|
res.end(JSON.stringify(receipt));
|
|
726
750
|
}
|
|
751
|
+
function handleApprove(req, res, approvalStore, expectedNonce) {
|
|
752
|
+
if (!approvalStore) {
|
|
753
|
+
res.writeHead(503);
|
|
754
|
+
res.end(JSON.stringify({ error: "approval_store_not_available" }));
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
let body = "";
|
|
758
|
+
req.on("data", (chunk) => {
|
|
759
|
+
body += chunk.toString();
|
|
760
|
+
});
|
|
761
|
+
req.on("end", () => {
|
|
762
|
+
try {
|
|
763
|
+
const { request_id, tool, mode, nonce } = JSON.parse(body);
|
|
764
|
+
if (expectedNonce && nonce !== expectedNonce) {
|
|
765
|
+
res.writeHead(403);
|
|
766
|
+
res.end(JSON.stringify({ error: "invalid_nonce", message: "Approval nonce does not match. Check stderr output for the correct nonce." }));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (!tool || typeof tool !== "string") {
|
|
770
|
+
res.writeHead(400);
|
|
771
|
+
res.end(JSON.stringify({ error: "missing_tool", usage: '{"request_id":"abc123","tool":"send_email","mode":"once|always","nonce":"..."}' }));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const grantMode = mode === "always" ? "always" : "once";
|
|
775
|
+
const ttlMs = grantMode === "once" ? 5 * 60 * 1e3 : 24 * 60 * 60 * 1e3;
|
|
776
|
+
const grantEntry = { tool, mode: grantMode, expires_at: Date.now() + ttlMs };
|
|
777
|
+
if (grantMode === "always") {
|
|
778
|
+
approvalStore.set(`always:${tool}`, grantEntry);
|
|
779
|
+
} else if (request_id) {
|
|
780
|
+
approvalStore.set(request_id, grantEntry);
|
|
781
|
+
} else {
|
|
782
|
+
approvalStore.set(tool, grantEntry);
|
|
783
|
+
}
|
|
784
|
+
res.writeHead(200);
|
|
785
|
+
res.end(JSON.stringify({
|
|
786
|
+
approved: true,
|
|
787
|
+
request_id: request_id || null,
|
|
788
|
+
tool,
|
|
789
|
+
mode: grantMode,
|
|
790
|
+
expires_in_seconds: ttlMs / 1e3
|
|
791
|
+
}));
|
|
792
|
+
} catch {
|
|
793
|
+
res.writeHead(400);
|
|
794
|
+
res.end(JSON.stringify({ error: "invalid_json", usage: '{"request_id":"abc123","tool":"send_email","mode":"once","nonce":"..."}' }));
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
function handleListApprovals(res, approvalStore) {
|
|
799
|
+
if (!approvalStore) {
|
|
800
|
+
res.writeHead(200);
|
|
801
|
+
res.end(JSON.stringify({ grants: [] }));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const now = Date.now();
|
|
805
|
+
const grants = [];
|
|
806
|
+
for (const [key, grant] of approvalStore) {
|
|
807
|
+
if (now < grant.expires_at) {
|
|
808
|
+
grants.push({ key, tool: grant.tool, mode: grant.mode, expires_in_seconds: Math.round((grant.expires_at - now) / 1e3) });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
res.writeHead(200);
|
|
812
|
+
res.end(JSON.stringify({ grants }));
|
|
813
|
+
}
|
|
727
814
|
|
|
728
815
|
// src/gateway.ts
|
|
729
816
|
var LOG_FILE2 = ".protect-mcp-log.jsonl";
|
|
817
|
+
var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
730
818
|
var ProtectGateway = class {
|
|
731
819
|
child = null;
|
|
732
820
|
config;
|
|
733
821
|
rateLimitStore = /* @__PURE__ */ new Map();
|
|
734
822
|
clientReader = null;
|
|
735
823
|
logFilePath;
|
|
824
|
+
receiptFilePath;
|
|
736
825
|
evidenceStore;
|
|
737
826
|
receiptBuffer;
|
|
827
|
+
/** Approval grants keyed by request_id (scoped to the specific action that was requested) */
|
|
828
|
+
approvalStore = /* @__PURE__ */ new Map();
|
|
829
|
+
/** Random nonce generated at startup — required for approval endpoint authentication */
|
|
830
|
+
approvalNonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
738
831
|
currentTier = "unknown";
|
|
739
832
|
admissionResult = null;
|
|
740
833
|
constructor(config) {
|
|
741
834
|
this.config = config;
|
|
742
835
|
this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
|
|
836
|
+
this.receiptFilePath = (0, import_node_path3.join)(process.cwd(), RECEIPTS_FILE);
|
|
743
837
|
this.evidenceStore = new EvidenceStore();
|
|
744
838
|
this.receiptBuffer = new ReceiptBuffer();
|
|
745
839
|
}
|
|
@@ -763,12 +857,15 @@ var ProtectGateway = class {
|
|
|
763
857
|
this.log(`External PDP: ${this.config.policy.external?.endpoint || "not configured"}`);
|
|
764
858
|
}
|
|
765
859
|
}
|
|
860
|
+
this.log(`Approval nonce: ${this.approvalNonce}`);
|
|
766
861
|
const httpPort = parseInt(process.env.PROTECT_MCP_HTTP_PORT || "9876", 10);
|
|
767
862
|
if (httpPort > 0) {
|
|
768
863
|
try {
|
|
769
864
|
startStatusServer(
|
|
770
865
|
{ port: httpPort, mode, verbose },
|
|
771
|
-
this.receiptBuffer
|
|
866
|
+
this.receiptBuffer,
|
|
867
|
+
this.approvalStore,
|
|
868
|
+
this.approvalNonce
|
|
772
869
|
);
|
|
773
870
|
} catch {
|
|
774
871
|
if (verbose) this.log(`HTTP status server could not start on port ${httpPort}`);
|
|
@@ -870,8 +967,31 @@ var ProtectGateway = class {
|
|
|
870
967
|
async interceptToolCall(request) {
|
|
871
968
|
const toolName = request.params?.name || "unknown";
|
|
872
969
|
const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
|
|
873
|
-
const toolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
874
970
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
971
|
+
let resolvedAgentKid = this.admissionResult?.agent_id;
|
|
972
|
+
let effectiveToolPolicy;
|
|
973
|
+
if (this.config.multiAgent?.enabled) {
|
|
974
|
+
const paramKid = request.params?._passport_kid;
|
|
975
|
+
if (paramKid) resolvedAgentKid = paramKid;
|
|
976
|
+
const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
|
|
977
|
+
if (agentOverrides && agentOverrides[toolName]) {
|
|
978
|
+
effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
|
|
979
|
+
} else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
|
|
980
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
|
|
981
|
+
if (this.config.enforce) {
|
|
982
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
|
|
983
|
+
}
|
|
984
|
+
return null;
|
|
985
|
+
} else {
|
|
986
|
+
effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
987
|
+
}
|
|
988
|
+
if (this.config.verbose && resolvedAgentKid) {
|
|
989
|
+
this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
993
|
+
}
|
|
994
|
+
const toolPolicy = effectiveToolPolicy;
|
|
875
995
|
let credentialRef;
|
|
876
996
|
if (this.config.credentials) {
|
|
877
997
|
const cred = resolveCredential(toolName, this.config.credentials);
|
|
@@ -922,6 +1042,32 @@ var ProtectGateway = class {
|
|
|
922
1042
|
}
|
|
923
1043
|
return null;
|
|
924
1044
|
}
|
|
1045
|
+
if (toolPolicy.require_approval) {
|
|
1046
|
+
const grant = this.approvalStore.get(requestId);
|
|
1047
|
+
const alwaysGrant = this.approvalStore.get(`always:${toolName}`);
|
|
1048
|
+
if (grant && Date.now() < grant.expires_at || alwaysGrant && Date.now() < alwaysGrant.expires_at) {
|
|
1049
|
+
if (grant && grant.mode === "once") this.approvalStore.delete(requestId);
|
|
1050
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
1054
|
+
if (this.config.enforce) {
|
|
1055
|
+
return {
|
|
1056
|
+
jsonrpc: "2.0",
|
|
1057
|
+
id: request.id,
|
|
1058
|
+
result: {
|
|
1059
|
+
content: [
|
|
1060
|
+
{
|
|
1061
|
+
type: "text",
|
|
1062
|
+
text: `REQUIRES_APPROVAL: The tool "${toolName}" requires human approval before execution. Request ID: ${requestId}. Approval nonce: ${this.approvalNonce}. Tell the user you need their approval to use "${toolName}" and will retry when granted. Do NOT retry this tool call until the user explicitly approves it.`
|
|
1063
|
+
}
|
|
1064
|
+
],
|
|
1065
|
+
isError: true
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
925
1071
|
const rateSpec = this.getTierRateLimit(toolPolicy, this.currentTier);
|
|
926
1072
|
if (rateSpec) {
|
|
927
1073
|
try {
|
|
@@ -979,6 +1125,10 @@ var ProtectGateway = class {
|
|
|
979
1125
|
if (signed.signed) {
|
|
980
1126
|
process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
|
|
981
1127
|
`);
|
|
1128
|
+
try {
|
|
1129
|
+
(0, import_node_fs5.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
982
1132
|
this.receiptBuffer.add(log.request_id, signed.signed);
|
|
983
1133
|
if (this.admissionResult?.agent_id) {
|
|
984
1134
|
this.evidenceStore.record(this.admissionResult.agent_id, this.config.signing?.issuer || "protect-mcp");
|
|
@@ -1065,6 +1215,298 @@ function collectSignedReceipts(logs) {
|
|
|
1065
1215
|
}).filter((r) => typeof r.signature === "string");
|
|
1066
1216
|
}
|
|
1067
1217
|
|
|
1218
|
+
// src/simulate.ts
|
|
1219
|
+
var import_node_fs6 = require("fs");
|
|
1220
|
+
function parseLogFile(path) {
|
|
1221
|
+
const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
|
|
1222
|
+
const entries = [];
|
|
1223
|
+
for (const line of raw.split("\n")) {
|
|
1224
|
+
const trimmed = line.trim();
|
|
1225
|
+
if (!trimmed) continue;
|
|
1226
|
+
const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
|
|
1227
|
+
try {
|
|
1228
|
+
const parsed = JSON.parse(jsonStr);
|
|
1229
|
+
if (parsed.tool && parsed.decision) {
|
|
1230
|
+
entries.push(parsed);
|
|
1231
|
+
}
|
|
1232
|
+
} catch {
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return entries;
|
|
1236
|
+
}
|
|
1237
|
+
function simulate(entries, policy, tier = "unknown") {
|
|
1238
|
+
const rateLimitStore = /* @__PURE__ */ new Map();
|
|
1239
|
+
const toolResults = /* @__PURE__ */ new Map();
|
|
1240
|
+
const totals = {
|
|
1241
|
+
allow: 0,
|
|
1242
|
+
block: 0,
|
|
1243
|
+
rate_limited: 0,
|
|
1244
|
+
require_approval: 0,
|
|
1245
|
+
tier_insufficient: 0
|
|
1246
|
+
};
|
|
1247
|
+
const originalTotals = { allow: 0, deny: 0 };
|
|
1248
|
+
const changes = [];
|
|
1249
|
+
for (const entry of entries) {
|
|
1250
|
+
const toolName = entry.tool;
|
|
1251
|
+
const toolPolicy = getToolPolicy(toolName, policy);
|
|
1252
|
+
if (entry.decision === "allow") {
|
|
1253
|
+
originalTotals.allow++;
|
|
1254
|
+
} else {
|
|
1255
|
+
originalTotals.deny++;
|
|
1256
|
+
}
|
|
1257
|
+
let newDecision;
|
|
1258
|
+
if (toolPolicy.block) {
|
|
1259
|
+
newDecision = "block";
|
|
1260
|
+
} else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
|
|
1261
|
+
newDecision = "tier_insufficient";
|
|
1262
|
+
} else if (toolPolicy.require_approval) {
|
|
1263
|
+
newDecision = "require_approval";
|
|
1264
|
+
} else if (toolPolicy.rate_limit) {
|
|
1265
|
+
const limit = parseRateLimit(toolPolicy.rate_limit);
|
|
1266
|
+
const result = checkRateLimit(toolName, limit, rateLimitStore);
|
|
1267
|
+
newDecision = result.allowed ? "allow" : "rate_limited";
|
|
1268
|
+
} else {
|
|
1269
|
+
newDecision = "allow";
|
|
1270
|
+
}
|
|
1271
|
+
totals[newDecision]++;
|
|
1272
|
+
if (!toolResults.has(toolName)) {
|
|
1273
|
+
toolResults.set(toolName, {
|
|
1274
|
+
tool: toolName,
|
|
1275
|
+
calls: 0,
|
|
1276
|
+
results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
|
|
1277
|
+
original: { allow: 0, deny: 0 }
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
const tr = toolResults.get(toolName);
|
|
1281
|
+
tr.calls++;
|
|
1282
|
+
tr.results[newDecision]++;
|
|
1283
|
+
if (entry.decision === "allow") {
|
|
1284
|
+
tr.original.allow++;
|
|
1285
|
+
} else {
|
|
1286
|
+
tr.original.deny++;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
for (const [tool, result] of toolResults) {
|
|
1290
|
+
const wasAllBlocked = result.original.allow === 0;
|
|
1291
|
+
const nowAllBlocked = result.results.allow === 0;
|
|
1292
|
+
const wasAllAllowed = result.original.deny === 0;
|
|
1293
|
+
if (wasAllAllowed && result.results.block > 0) {
|
|
1294
|
+
changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
|
|
1295
|
+
}
|
|
1296
|
+
if (wasAllAllowed && result.results.rate_limited > 0) {
|
|
1297
|
+
changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
|
|
1298
|
+
}
|
|
1299
|
+
if (wasAllAllowed && result.results.require_approval > 0) {
|
|
1300
|
+
changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
|
|
1301
|
+
}
|
|
1302
|
+
if (wasAllAllowed && result.results.tier_insufficient > 0) {
|
|
1303
|
+
changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
|
|
1304
|
+
}
|
|
1305
|
+
if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
|
|
1306
|
+
changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
policy_file: "",
|
|
1311
|
+
log_file: "",
|
|
1312
|
+
total_calls: entries.length,
|
|
1313
|
+
results: totals,
|
|
1314
|
+
original: originalTotals,
|
|
1315
|
+
tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
|
|
1316
|
+
changes
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function formatSimulation(summary) {
|
|
1320
|
+
const lines = [];
|
|
1321
|
+
lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
|
|
1322
|
+
`);
|
|
1323
|
+
const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
|
|
1324
|
+
for (const tr of summary.tool_breakdown) {
|
|
1325
|
+
const parts = [];
|
|
1326
|
+
if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
|
|
1327
|
+
if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
|
|
1328
|
+
if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
|
|
1329
|
+
if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
|
|
1330
|
+
if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
|
|
1331
|
+
const originalParts = [];
|
|
1332
|
+
if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
|
|
1333
|
+
if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
|
|
1334
|
+
lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
|
|
1335
|
+
}
|
|
1336
|
+
lines.push("");
|
|
1337
|
+
lines.push(`Summary: ${summary.results.allow} allow, ${summary.results.block} blocked, ${summary.results.rate_limited} rate_limited, ${summary.results.require_approval} require_approval, ${summary.results.tier_insufficient} tier_insufficient`);
|
|
1338
|
+
lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
|
|
1339
|
+
if (summary.changes.length > 0) {
|
|
1340
|
+
lines.push("");
|
|
1341
|
+
lines.push("Changes:");
|
|
1342
|
+
for (const change of summary.changes) {
|
|
1343
|
+
lines.push(` \u2022 ${change}`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return lines.join("\n");
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/report.ts
|
|
1350
|
+
var import_node_fs7 = require("fs");
|
|
1351
|
+
function generateReport(logPath, receiptPath, periodDays) {
|
|
1352
|
+
const now = /* @__PURE__ */ new Date();
|
|
1353
|
+
const from = new Date(now.getTime() - periodDays * 864e5);
|
|
1354
|
+
const entries = [];
|
|
1355
|
+
if ((0, import_node_fs7.existsSync)(logPath)) {
|
|
1356
|
+
const raw = (0, import_node_fs7.readFileSync)(logPath, "utf-8");
|
|
1357
|
+
for (const line of raw.split("\n")) {
|
|
1358
|
+
const trimmed = line.trim();
|
|
1359
|
+
if (!trimmed) continue;
|
|
1360
|
+
const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
|
|
1361
|
+
try {
|
|
1362
|
+
const parsed = JSON.parse(jsonStr);
|
|
1363
|
+
if (parsed.tool && parsed.decision && parsed.timestamp) {
|
|
1364
|
+
const entryTime = typeof parsed.timestamp === "number" && parsed.timestamp > 1e12 ? parsed.timestamp : parsed.timestamp * 1e3;
|
|
1365
|
+
if (entryTime >= from.getTime()) {
|
|
1366
|
+
entries.push(parsed);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
} catch {
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
let receiptsSigned = 0;
|
|
1374
|
+
let signerKid = "";
|
|
1375
|
+
let signerIssuer = "";
|
|
1376
|
+
if ((0, import_node_fs7.existsSync)(receiptPath)) {
|
|
1377
|
+
const raw = (0, import_node_fs7.readFileSync)(receiptPath, "utf-8");
|
|
1378
|
+
for (const line of raw.split("\n")) {
|
|
1379
|
+
const trimmed = line.trim();
|
|
1380
|
+
if (!trimmed) continue;
|
|
1381
|
+
try {
|
|
1382
|
+
const parsed = JSON.parse(trimmed);
|
|
1383
|
+
if (parsed.signature) {
|
|
1384
|
+
receiptsSigned++;
|
|
1385
|
+
if (parsed.kid && !signerKid) signerKid = parsed.kid;
|
|
1386
|
+
if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
|
|
1387
|
+
}
|
|
1388
|
+
} catch {
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
1393
|
+
const tiers = /* @__PURE__ */ new Set();
|
|
1394
|
+
const policyDigests = /* @__PURE__ */ new Map();
|
|
1395
|
+
let allowed = 0;
|
|
1396
|
+
let blocked = 0;
|
|
1397
|
+
let rateLimited = 0;
|
|
1398
|
+
let approvalRequired = 0;
|
|
1399
|
+
for (const entry of entries) {
|
|
1400
|
+
const tool = entry.tool;
|
|
1401
|
+
if (!toolMap.has(tool)) {
|
|
1402
|
+
toolMap.set(tool, { total: 0, allowed: 0, blocked: 0, rate_limited: 0, approval_required: 0 });
|
|
1403
|
+
}
|
|
1404
|
+
const tm = toolMap.get(tool);
|
|
1405
|
+
tm.total++;
|
|
1406
|
+
if (entry.decision === "allow") {
|
|
1407
|
+
allowed++;
|
|
1408
|
+
tm.allowed++;
|
|
1409
|
+
} else if (entry.decision === "deny" && entry.reason_code === "rate_limit_exceeded") {
|
|
1410
|
+
rateLimited++;
|
|
1411
|
+
tm.rate_limited++;
|
|
1412
|
+
} else if (entry.decision === "deny" && entry.reason_code === "require_approval") {
|
|
1413
|
+
approvalRequired++;
|
|
1414
|
+
tm.approval_required++;
|
|
1415
|
+
} else {
|
|
1416
|
+
blocked++;
|
|
1417
|
+
tm.blocked++;
|
|
1418
|
+
}
|
|
1419
|
+
if (entry.tier) tiers.add(entry.tier);
|
|
1420
|
+
if (entry.policy_digest && !policyDigests.has(entry.policy_digest)) {
|
|
1421
|
+
policyDigests.set(entry.policy_digest, new Date(entry.timestamp).toISOString());
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const policyChanges = Array.from(policyDigests.entries()).map(([digest, at]) => ({
|
|
1425
|
+
at,
|
|
1426
|
+
policy_digest: digest
|
|
1427
|
+
})).sort((a, b) => a.at.localeCompare(b.at));
|
|
1428
|
+
return {
|
|
1429
|
+
generated_at: now.toISOString(),
|
|
1430
|
+
period: { from: from.toISOString(), to: now.toISOString() },
|
|
1431
|
+
signing_identity: signerKid ? { kid: signerKid, issuer: signerIssuer } : null,
|
|
1432
|
+
summary: {
|
|
1433
|
+
total_decisions: entries.length,
|
|
1434
|
+
allowed,
|
|
1435
|
+
blocked,
|
|
1436
|
+
rate_limited: rateLimited,
|
|
1437
|
+
approval_required: approvalRequired,
|
|
1438
|
+
unique_tools: toolMap.size,
|
|
1439
|
+
unique_tiers: tiers.size
|
|
1440
|
+
},
|
|
1441
|
+
tool_breakdown: Array.from(toolMap.entries()).map(([tool, stats]) => ({ tool, ...stats })).sort((a, b) => b.total - a.total),
|
|
1442
|
+
policy_changes: policyChanges,
|
|
1443
|
+
verification: {
|
|
1444
|
+
receipts_signed: receiptsSigned,
|
|
1445
|
+
receipts_unsigned: entries.length - receiptsSigned,
|
|
1446
|
+
verify_command: "npx @veritasacta/verify audit-bundle.json --bundle"
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function formatReportMarkdown(report) {
|
|
1451
|
+
const lines = [];
|
|
1452
|
+
lines.push("# ScopeBlind Compliance Report");
|
|
1453
|
+
lines.push("");
|
|
1454
|
+
lines.push(`**Generated:** ${report.generated_at}`);
|
|
1455
|
+
lines.push(`**Period:** ${report.period.from.split("T")[0]} to ${report.period.to.split("T")[0]}`);
|
|
1456
|
+
if (report.signing_identity) {
|
|
1457
|
+
lines.push(`**Signing identity:** kid \`${report.signing_identity.kid}\`, issuer \`${report.signing_identity.issuer}\``);
|
|
1458
|
+
}
|
|
1459
|
+
lines.push("");
|
|
1460
|
+
lines.push("## Summary");
|
|
1461
|
+
lines.push("");
|
|
1462
|
+
lines.push(`| Metric | Value |`);
|
|
1463
|
+
lines.push(`|--------|-------|`);
|
|
1464
|
+
lines.push(`| Total decisions | ${report.summary.total_decisions} |`);
|
|
1465
|
+
lines.push(`| Allowed | ${report.summary.allowed} |`);
|
|
1466
|
+
lines.push(`| Blocked | ${report.summary.blocked} |`);
|
|
1467
|
+
lines.push(`| Rate-limited | ${report.summary.rate_limited} |`);
|
|
1468
|
+
lines.push(`| Approval required | ${report.summary.approval_required} |`);
|
|
1469
|
+
lines.push(`| Unique tools | ${report.summary.unique_tools} |`);
|
|
1470
|
+
lines.push(`| Unique tiers | ${report.summary.unique_tiers} |`);
|
|
1471
|
+
lines.push("");
|
|
1472
|
+
if (report.tool_breakdown.length > 0) {
|
|
1473
|
+
lines.push("## Tool Breakdown");
|
|
1474
|
+
lines.push("");
|
|
1475
|
+
lines.push("| Tool | Total | Allowed | Blocked | Rate-limited | Approval |");
|
|
1476
|
+
lines.push("|------|-------|---------|---------|--------------|----------|");
|
|
1477
|
+
for (const t of report.tool_breakdown) {
|
|
1478
|
+
lines.push(`| \`${t.tool}\` | ${t.total} | ${t.allowed} | ${t.blocked} | ${t.rate_limited} | ${t.approval_required} |`);
|
|
1479
|
+
}
|
|
1480
|
+
lines.push("");
|
|
1481
|
+
}
|
|
1482
|
+
if (report.policy_changes.length > 0) {
|
|
1483
|
+
lines.push("## Policy History");
|
|
1484
|
+
lines.push("");
|
|
1485
|
+
lines.push("| Timestamp | Policy Digest |");
|
|
1486
|
+
lines.push("|-----------|--------------|");
|
|
1487
|
+
for (const pc of report.policy_changes) {
|
|
1488
|
+
lines.push(`| ${pc.at} | \`${pc.policy_digest}\` |`);
|
|
1489
|
+
}
|
|
1490
|
+
lines.push("");
|
|
1491
|
+
}
|
|
1492
|
+
lines.push("## Verification");
|
|
1493
|
+
lines.push("");
|
|
1494
|
+
lines.push(`- Receipts signed: **${report.verification.receipts_signed}**`);
|
|
1495
|
+
lines.push(`- Receipts unsigned: **${report.verification.receipts_unsigned}**`);
|
|
1496
|
+
lines.push("");
|
|
1497
|
+
lines.push("Verify the audit bundle:");
|
|
1498
|
+
lines.push("");
|
|
1499
|
+
lines.push("```bash");
|
|
1500
|
+
lines.push(report.verification.verify_command);
|
|
1501
|
+
lines.push("```");
|
|
1502
|
+
lines.push("");
|
|
1503
|
+
lines.push("The verifier is MIT-licensed and works offline. No ScopeBlind account required.");
|
|
1504
|
+
lines.push("");
|
|
1505
|
+
lines.push("---");
|
|
1506
|
+
lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
|
|
1507
|
+
return lines.join("\n");
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1068
1510
|
// src/manifest.ts
|
|
1069
1511
|
function isAgentId(s) {
|
|
1070
1512
|
return /^sb:agent:[a-f0-9]{32}$/.test(s);
|
|
@@ -1227,6 +1669,9 @@ function validateEvidenceReceipt(receipt) {
|
|
|
1227
1669
|
collectSignedReceipts,
|
|
1228
1670
|
createAuditBundle,
|
|
1229
1671
|
evaluateTier,
|
|
1672
|
+
formatReportMarkdown,
|
|
1673
|
+
formatSimulation,
|
|
1674
|
+
generateReport,
|
|
1230
1675
|
getSignerInfo,
|
|
1231
1676
|
getToolPolicy,
|
|
1232
1677
|
initSigning,
|
|
@@ -1238,10 +1683,12 @@ function validateEvidenceReceipt(receipt) {
|
|
|
1238
1683
|
listCredentialLabels,
|
|
1239
1684
|
loadPolicy,
|
|
1240
1685
|
meetsMinTier,
|
|
1686
|
+
parseLogFile,
|
|
1241
1687
|
parseRateLimit,
|
|
1242
1688
|
queryExternalPDP,
|
|
1243
1689
|
resolveCredential,
|
|
1244
1690
|
signDecision,
|
|
1691
|
+
simulate,
|
|
1245
1692
|
validateCredentials,
|
|
1246
1693
|
validateEvidenceReceipt,
|
|
1247
1694
|
validateManifest
|