protect-mcp 0.3.1 → 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/dist/{chunk-U7TMVD3E.mjs → chunk-GV7N53QE.mjs} +164 -3
- package/dist/chunk-JQDVKZBN.mjs +165 -0
- package/dist/cli.js +766 -31
- package/dist/cli.mjs +414 -6
- package/dist/index.d.mts +148 -1
- package/dist/index.d.ts +148 -1
- package/dist/index.js +331 -12
- package/dist/index.mjs +13 -1
- package/dist/report-ENQ3KUI2.mjs +8 -0
- package/package.json +2 -2
|
@@ -317,7 +317,11 @@ async function initSigning(config) {
|
|
|
317
317
|
return warnings;
|
|
318
318
|
}
|
|
319
319
|
try {
|
|
320
|
-
|
|
320
|
+
const moduleName = "@veritasacta/artifacts";
|
|
321
|
+
artifactsModule = await import(
|
|
322
|
+
/* @vite-ignore */
|
|
323
|
+
moduleName
|
|
324
|
+
);
|
|
321
325
|
} catch {
|
|
322
326
|
warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
|
|
323
327
|
return warnings;
|
|
@@ -909,8 +913,31 @@ var ProtectGateway = class {
|
|
|
909
913
|
async interceptToolCall(request) {
|
|
910
914
|
const toolName = request.params?.name || "unknown";
|
|
911
915
|
const requestId = randomUUID().slice(0, 12);
|
|
912
|
-
const toolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
913
916
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
917
|
+
let resolvedAgentKid = this.admissionResult?.agent_id;
|
|
918
|
+
let effectiveToolPolicy;
|
|
919
|
+
if (this.config.multiAgent?.enabled) {
|
|
920
|
+
const paramKid = request.params?._passport_kid;
|
|
921
|
+
if (paramKid) resolvedAgentKid = paramKid;
|
|
922
|
+
const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
|
|
923
|
+
if (agentOverrides && agentOverrides[toolName]) {
|
|
924
|
+
effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
|
|
925
|
+
} else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
|
|
926
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
|
|
927
|
+
if (this.config.enforce) {
|
|
928
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
|
|
929
|
+
}
|
|
930
|
+
return null;
|
|
931
|
+
} else {
|
|
932
|
+
effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
933
|
+
}
|
|
934
|
+
if (this.config.verbose && resolvedAgentKid) {
|
|
935
|
+
this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
939
|
+
}
|
|
940
|
+
const toolPolicy = effectiveToolPolicy;
|
|
914
941
|
let credentialRef;
|
|
915
942
|
if (this.config.credentials) {
|
|
916
943
|
const cred = resolveCredential(toolName, this.config.credentials);
|
|
@@ -1085,6 +1112,137 @@ var ProtectGateway = class {
|
|
|
1085
1112
|
}
|
|
1086
1113
|
};
|
|
1087
1114
|
|
|
1115
|
+
// src/simulate.ts
|
|
1116
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1117
|
+
function parseLogFile(path) {
|
|
1118
|
+
const raw = readFileSync5(path, "utf-8");
|
|
1119
|
+
const entries = [];
|
|
1120
|
+
for (const line of raw.split("\n")) {
|
|
1121
|
+
const trimmed = line.trim();
|
|
1122
|
+
if (!trimmed) continue;
|
|
1123
|
+
const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
|
|
1124
|
+
try {
|
|
1125
|
+
const parsed = JSON.parse(jsonStr);
|
|
1126
|
+
if (parsed.tool && parsed.decision) {
|
|
1127
|
+
entries.push(parsed);
|
|
1128
|
+
}
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return entries;
|
|
1133
|
+
}
|
|
1134
|
+
function simulate(entries, policy, tier = "unknown") {
|
|
1135
|
+
const rateLimitStore = /* @__PURE__ */ new Map();
|
|
1136
|
+
const toolResults = /* @__PURE__ */ new Map();
|
|
1137
|
+
const totals = {
|
|
1138
|
+
allow: 0,
|
|
1139
|
+
block: 0,
|
|
1140
|
+
rate_limited: 0,
|
|
1141
|
+
require_approval: 0,
|
|
1142
|
+
tier_insufficient: 0
|
|
1143
|
+
};
|
|
1144
|
+
const originalTotals = { allow: 0, deny: 0 };
|
|
1145
|
+
const changes = [];
|
|
1146
|
+
for (const entry of entries) {
|
|
1147
|
+
const toolName = entry.tool;
|
|
1148
|
+
const toolPolicy = getToolPolicy(toolName, policy);
|
|
1149
|
+
if (entry.decision === "allow") {
|
|
1150
|
+
originalTotals.allow++;
|
|
1151
|
+
} else {
|
|
1152
|
+
originalTotals.deny++;
|
|
1153
|
+
}
|
|
1154
|
+
let newDecision;
|
|
1155
|
+
if (toolPolicy.block) {
|
|
1156
|
+
newDecision = "block";
|
|
1157
|
+
} else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
|
|
1158
|
+
newDecision = "tier_insufficient";
|
|
1159
|
+
} else if (toolPolicy.require_approval) {
|
|
1160
|
+
newDecision = "require_approval";
|
|
1161
|
+
} else if (toolPolicy.rate_limit) {
|
|
1162
|
+
const limit = parseRateLimit(toolPolicy.rate_limit);
|
|
1163
|
+
const result = checkRateLimit(toolName, limit, rateLimitStore);
|
|
1164
|
+
newDecision = result.allowed ? "allow" : "rate_limited";
|
|
1165
|
+
} else {
|
|
1166
|
+
newDecision = "allow";
|
|
1167
|
+
}
|
|
1168
|
+
totals[newDecision]++;
|
|
1169
|
+
if (!toolResults.has(toolName)) {
|
|
1170
|
+
toolResults.set(toolName, {
|
|
1171
|
+
tool: toolName,
|
|
1172
|
+
calls: 0,
|
|
1173
|
+
results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
|
|
1174
|
+
original: { allow: 0, deny: 0 }
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
const tr = toolResults.get(toolName);
|
|
1178
|
+
tr.calls++;
|
|
1179
|
+
tr.results[newDecision]++;
|
|
1180
|
+
if (entry.decision === "allow") {
|
|
1181
|
+
tr.original.allow++;
|
|
1182
|
+
} else {
|
|
1183
|
+
tr.original.deny++;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
for (const [tool, result] of toolResults) {
|
|
1187
|
+
const wasAllBlocked = result.original.allow === 0;
|
|
1188
|
+
const nowAllBlocked = result.results.allow === 0;
|
|
1189
|
+
const wasAllAllowed = result.original.deny === 0;
|
|
1190
|
+
if (wasAllAllowed && result.results.block > 0) {
|
|
1191
|
+
changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
|
|
1192
|
+
}
|
|
1193
|
+
if (wasAllAllowed && result.results.rate_limited > 0) {
|
|
1194
|
+
changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
|
|
1195
|
+
}
|
|
1196
|
+
if (wasAllAllowed && result.results.require_approval > 0) {
|
|
1197
|
+
changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
|
|
1198
|
+
}
|
|
1199
|
+
if (wasAllAllowed && result.results.tier_insufficient > 0) {
|
|
1200
|
+
changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
|
|
1201
|
+
}
|
|
1202
|
+
if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
|
|
1203
|
+
changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return {
|
|
1207
|
+
policy_file: "",
|
|
1208
|
+
log_file: "",
|
|
1209
|
+
total_calls: entries.length,
|
|
1210
|
+
results: totals,
|
|
1211
|
+
original: originalTotals,
|
|
1212
|
+
tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
|
|
1213
|
+
changes
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function formatSimulation(summary) {
|
|
1217
|
+
const lines = [];
|
|
1218
|
+
lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
|
|
1219
|
+
`);
|
|
1220
|
+
const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
|
|
1221
|
+
for (const tr of summary.tool_breakdown) {
|
|
1222
|
+
const parts = [];
|
|
1223
|
+
if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
|
|
1224
|
+
if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
|
|
1225
|
+
if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
|
|
1226
|
+
if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
|
|
1227
|
+
if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
|
|
1228
|
+
const originalParts = [];
|
|
1229
|
+
if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
|
|
1230
|
+
if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
|
|
1231
|
+
lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
|
|
1232
|
+
}
|
|
1233
|
+
lines.push("");
|
|
1234
|
+
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`);
|
|
1235
|
+
lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
|
|
1236
|
+
if (summary.changes.length > 0) {
|
|
1237
|
+
lines.push("");
|
|
1238
|
+
lines.push("Changes:");
|
|
1239
|
+
for (const change of summary.changes) {
|
|
1240
|
+
lines.push(` \u2022 ${change}`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return lines.join("\n");
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1088
1246
|
export {
|
|
1089
1247
|
loadPolicy,
|
|
1090
1248
|
getToolPolicy,
|
|
@@ -1101,5 +1259,8 @@ export {
|
|
|
1101
1259
|
isSigningEnabled,
|
|
1102
1260
|
queryExternalPDP,
|
|
1103
1261
|
buildDecisionContext,
|
|
1104
|
-
ProtectGateway
|
|
1262
|
+
ProtectGateway,
|
|
1263
|
+
parseLogFile,
|
|
1264
|
+
simulate,
|
|
1265
|
+
formatSimulation
|
|
1105
1266
|
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// src/report.ts
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
function generateReport(logPath, receiptPath, periodDays) {
|
|
4
|
+
const now = /* @__PURE__ */ new Date();
|
|
5
|
+
const from = new Date(now.getTime() - periodDays * 864e5);
|
|
6
|
+
const entries = [];
|
|
7
|
+
if (existsSync(logPath)) {
|
|
8
|
+
const raw = readFileSync(logPath, "utf-8");
|
|
9
|
+
for (const line of raw.split("\n")) {
|
|
10
|
+
const trimmed = line.trim();
|
|
11
|
+
if (!trimmed) continue;
|
|
12
|
+
const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(jsonStr);
|
|
15
|
+
if (parsed.tool && parsed.decision && parsed.timestamp) {
|
|
16
|
+
const entryTime = typeof parsed.timestamp === "number" && parsed.timestamp > 1e12 ? parsed.timestamp : parsed.timestamp * 1e3;
|
|
17
|
+
if (entryTime >= from.getTime()) {
|
|
18
|
+
entries.push(parsed);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
let receiptsSigned = 0;
|
|
26
|
+
let signerKid = "";
|
|
27
|
+
let signerIssuer = "";
|
|
28
|
+
if (existsSync(receiptPath)) {
|
|
29
|
+
const raw = readFileSync(receiptPath, "utf-8");
|
|
30
|
+
for (const line of raw.split("\n")) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (!trimmed) continue;
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(trimmed);
|
|
35
|
+
if (parsed.signature) {
|
|
36
|
+
receiptsSigned++;
|
|
37
|
+
if (parsed.kid && !signerKid) signerKid = parsed.kid;
|
|
38
|
+
if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
45
|
+
const tiers = /* @__PURE__ */ new Set();
|
|
46
|
+
const policyDigests = /* @__PURE__ */ new Map();
|
|
47
|
+
let allowed = 0;
|
|
48
|
+
let blocked = 0;
|
|
49
|
+
let rateLimited = 0;
|
|
50
|
+
let approvalRequired = 0;
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const tool = entry.tool;
|
|
53
|
+
if (!toolMap.has(tool)) {
|
|
54
|
+
toolMap.set(tool, { total: 0, allowed: 0, blocked: 0, rate_limited: 0, approval_required: 0 });
|
|
55
|
+
}
|
|
56
|
+
const tm = toolMap.get(tool);
|
|
57
|
+
tm.total++;
|
|
58
|
+
if (entry.decision === "allow") {
|
|
59
|
+
allowed++;
|
|
60
|
+
tm.allowed++;
|
|
61
|
+
} else if (entry.decision === "deny" && entry.reason_code === "rate_limit_exceeded") {
|
|
62
|
+
rateLimited++;
|
|
63
|
+
tm.rate_limited++;
|
|
64
|
+
} else if (entry.decision === "deny" && entry.reason_code === "require_approval") {
|
|
65
|
+
approvalRequired++;
|
|
66
|
+
tm.approval_required++;
|
|
67
|
+
} else {
|
|
68
|
+
blocked++;
|
|
69
|
+
tm.blocked++;
|
|
70
|
+
}
|
|
71
|
+
if (entry.tier) tiers.add(entry.tier);
|
|
72
|
+
if (entry.policy_digest && !policyDigests.has(entry.policy_digest)) {
|
|
73
|
+
policyDigests.set(entry.policy_digest, new Date(entry.timestamp).toISOString());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const policyChanges = Array.from(policyDigests.entries()).map(([digest, at]) => ({
|
|
77
|
+
at,
|
|
78
|
+
policy_digest: digest
|
|
79
|
+
})).sort((a, b) => a.at.localeCompare(b.at));
|
|
80
|
+
return {
|
|
81
|
+
generated_at: now.toISOString(),
|
|
82
|
+
period: { from: from.toISOString(), to: now.toISOString() },
|
|
83
|
+
signing_identity: signerKid ? { kid: signerKid, issuer: signerIssuer } : null,
|
|
84
|
+
summary: {
|
|
85
|
+
total_decisions: entries.length,
|
|
86
|
+
allowed,
|
|
87
|
+
blocked,
|
|
88
|
+
rate_limited: rateLimited,
|
|
89
|
+
approval_required: approvalRequired,
|
|
90
|
+
unique_tools: toolMap.size,
|
|
91
|
+
unique_tiers: tiers.size
|
|
92
|
+
},
|
|
93
|
+
tool_breakdown: Array.from(toolMap.entries()).map(([tool, stats]) => ({ tool, ...stats })).sort((a, b) => b.total - a.total),
|
|
94
|
+
policy_changes: policyChanges,
|
|
95
|
+
verification: {
|
|
96
|
+
receipts_signed: receiptsSigned,
|
|
97
|
+
receipts_unsigned: entries.length - receiptsSigned,
|
|
98
|
+
verify_command: "npx @veritasacta/verify audit-bundle.json --bundle"
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function formatReportMarkdown(report) {
|
|
103
|
+
const lines = [];
|
|
104
|
+
lines.push("# ScopeBlind Compliance Report");
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push(`**Generated:** ${report.generated_at}`);
|
|
107
|
+
lines.push(`**Period:** ${report.period.from.split("T")[0]} to ${report.period.to.split("T")[0]}`);
|
|
108
|
+
if (report.signing_identity) {
|
|
109
|
+
lines.push(`**Signing identity:** kid \`${report.signing_identity.kid}\`, issuer \`${report.signing_identity.issuer}\``);
|
|
110
|
+
}
|
|
111
|
+
lines.push("");
|
|
112
|
+
lines.push("## Summary");
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(`| Metric | Value |`);
|
|
115
|
+
lines.push(`|--------|-------|`);
|
|
116
|
+
lines.push(`| Total decisions | ${report.summary.total_decisions} |`);
|
|
117
|
+
lines.push(`| Allowed | ${report.summary.allowed} |`);
|
|
118
|
+
lines.push(`| Blocked | ${report.summary.blocked} |`);
|
|
119
|
+
lines.push(`| Rate-limited | ${report.summary.rate_limited} |`);
|
|
120
|
+
lines.push(`| Approval required | ${report.summary.approval_required} |`);
|
|
121
|
+
lines.push(`| Unique tools | ${report.summary.unique_tools} |`);
|
|
122
|
+
lines.push(`| Unique tiers | ${report.summary.unique_tiers} |`);
|
|
123
|
+
lines.push("");
|
|
124
|
+
if (report.tool_breakdown.length > 0) {
|
|
125
|
+
lines.push("## Tool Breakdown");
|
|
126
|
+
lines.push("");
|
|
127
|
+
lines.push("| Tool | Total | Allowed | Blocked | Rate-limited | Approval |");
|
|
128
|
+
lines.push("|------|-------|---------|---------|--------------|----------|");
|
|
129
|
+
for (const t of report.tool_breakdown) {
|
|
130
|
+
lines.push(`| \`${t.tool}\` | ${t.total} | ${t.allowed} | ${t.blocked} | ${t.rate_limited} | ${t.approval_required} |`);
|
|
131
|
+
}
|
|
132
|
+
lines.push("");
|
|
133
|
+
}
|
|
134
|
+
if (report.policy_changes.length > 0) {
|
|
135
|
+
lines.push("## Policy History");
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push("| Timestamp | Policy Digest |");
|
|
138
|
+
lines.push("|-----------|--------------|");
|
|
139
|
+
for (const pc of report.policy_changes) {
|
|
140
|
+
lines.push(`| ${pc.at} | \`${pc.policy_digest}\` |`);
|
|
141
|
+
}
|
|
142
|
+
lines.push("");
|
|
143
|
+
}
|
|
144
|
+
lines.push("## Verification");
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push(`- Receipts signed: **${report.verification.receipts_signed}**`);
|
|
147
|
+
lines.push(`- Receipts unsigned: **${report.verification.receipts_unsigned}**`);
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push("Verify the audit bundle:");
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push("```bash");
|
|
152
|
+
lines.push(report.verification.verify_command);
|
|
153
|
+
lines.push("```");
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push("The verifier is MIT-licensed and works offline. No ScopeBlind account required.");
|
|
156
|
+
lines.push("");
|
|
157
|
+
lines.push("---");
|
|
158
|
+
lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export {
|
|
163
|
+
generateReport,
|
|
164
|
+
formatReportMarkdown
|
|
165
|
+
};
|