protect-mcp 0.3.1 → 0.3.3
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/LICENSE +68 -0
- package/README.md +42 -2
- package/dist/chunk-JQDVKZBN.mjs +165 -0
- package/dist/{chunk-U7TMVD3E.mjs → chunk-WDCPUM2O.mjs} +202 -3
- package/dist/cli.js +804 -31
- package/dist/cli.mjs +414 -6
- package/dist/index.d.mts +151 -4
- package/dist/index.d.ts +151 -4
- package/dist/index.js +369 -12
- package/dist/index.mjs +13 -1
- package/dist/report-ENQ3KUI2.mjs +8 -0
- package/package.json +13 -3
- package/policies/claude-code-hooks.json +18 -0
- package/policies/clinejection.json +45 -0
- package/policies/data-exfiltration.json +52 -0
- package/policies/financial-safe.json +49 -0
- package/policies/github-mcp-hijack.json +54 -0
- package/policies/terraform-destroy.json +50 -0
package/dist/index.d.ts
CHANGED
|
@@ -36,8 +36,8 @@ type PolicyEngineMode = 'built-in' | 'external' | 'hybrid';
|
|
|
36
36
|
interface ExternalPDPConfig {
|
|
37
37
|
/** HTTP endpoint for the external policy decision point */
|
|
38
38
|
endpoint: string;
|
|
39
|
-
/** Response format: 'opa' | 'cerbos' | 'generic' */
|
|
40
|
-
format?: 'opa' | 'cerbos' | 'generic';
|
|
39
|
+
/** Response format: 'opa' | 'cerbos' | 'cedar' | 'generic' */
|
|
40
|
+
format?: 'opa' | 'cerbos' | 'cedar' | 'generic';
|
|
41
41
|
/** Timeout in milliseconds (default: 500) */
|
|
42
42
|
timeout_ms?: number;
|
|
43
43
|
/** Fallback decision when external PDP is unreachable */
|
|
@@ -154,6 +154,27 @@ interface ProtectConfig {
|
|
|
154
154
|
signing?: SigningConfig;
|
|
155
155
|
/** Credential vault: maps credential labels to injection config */
|
|
156
156
|
credentials?: Record<string, CredentialConfig>;
|
|
157
|
+
/** Multi-agent mode: identify calling agents and apply per-agent policy */
|
|
158
|
+
multiAgent?: MultiAgentConfig;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Multi-agent mode configuration.
|
|
162
|
+
*
|
|
163
|
+
* When enabled, protect-mcp resolves the calling agent's passport kid
|
|
164
|
+
* from request metadata (x-passport-kid header or _passport_kid param)
|
|
165
|
+
* and applies agent-specific policy overrides.
|
|
166
|
+
*/
|
|
167
|
+
interface MultiAgentConfig {
|
|
168
|
+
/** Enable multi-agent mode */
|
|
169
|
+
enabled: boolean;
|
|
170
|
+
/** Registry endpoint for agent manifest lookup */
|
|
171
|
+
registryUrl?: string;
|
|
172
|
+
/** Per-agent policy overrides: maps kid → tool policy overrides */
|
|
173
|
+
agentPolicies?: Record<string, Record<string, ToolPolicy>>;
|
|
174
|
+
/** Default policy for unrecognized agents (default: use base policy) */
|
|
175
|
+
unknownAgentPolicy?: 'base' | 'deny' | 'shadow-only';
|
|
176
|
+
/** Cache TTL for agent manifests in ms (default: 300000 = 5 min) */
|
|
177
|
+
cacheTtlMs?: number;
|
|
157
178
|
}
|
|
158
179
|
|
|
159
180
|
/**
|
|
@@ -459,7 +480,7 @@ declare function isSigningEnabled(): boolean;
|
|
|
459
480
|
* BYOPE (Bring Your Own Policy Engine) — sends decision context
|
|
460
481
|
* to an external Policy Decision Point via HTTP webhook.
|
|
461
482
|
*
|
|
462
|
-
* Supports OPA, Cerbos, and generic JSON formats.
|
|
483
|
+
* Supports OPA, Cerbos, Cedar (AWS), and generic JSON formats.
|
|
463
484
|
* ScopeBlind always signs the receipt regardless of who made the decision.
|
|
464
485
|
*
|
|
465
486
|
* Sprint 2: One HTTP webhook adapter. More adapters later.
|
|
@@ -570,6 +591,132 @@ declare function createAuditBundle(opts: AuditBundleOptions): AuditBundle;
|
|
|
570
591
|
*/
|
|
571
592
|
declare function collectSignedReceipts(logs: DecisionLog[]): Record<string, unknown>[];
|
|
572
593
|
|
|
594
|
+
/**
|
|
595
|
+
* protect-mcp simulate — dry-run policy evaluation
|
|
596
|
+
*
|
|
597
|
+
* Reads a recorded log file (.protect-mcp-log.jsonl) and evaluates
|
|
598
|
+
* each tool call against a policy file. Shows what would have been
|
|
599
|
+
* blocked, rate-limited, or approved — without wrapping a live server.
|
|
600
|
+
*
|
|
601
|
+
* Usage:
|
|
602
|
+
* npx protect-mcp simulate --policy strict.json [--log .protect-mcp-log.jsonl] [--json]
|
|
603
|
+
*/
|
|
604
|
+
|
|
605
|
+
interface LogEntry {
|
|
606
|
+
v: number;
|
|
607
|
+
tool: string;
|
|
608
|
+
decision: string;
|
|
609
|
+
reason_code: string;
|
|
610
|
+
mode: string;
|
|
611
|
+
timestamp: number;
|
|
612
|
+
tier?: string;
|
|
613
|
+
rate_limit_remaining?: number;
|
|
614
|
+
[key: string]: unknown;
|
|
615
|
+
}
|
|
616
|
+
interface SimulationResult {
|
|
617
|
+
tool: string;
|
|
618
|
+
calls: number;
|
|
619
|
+
results: {
|
|
620
|
+
allow: number;
|
|
621
|
+
block: number;
|
|
622
|
+
rate_limited: number;
|
|
623
|
+
require_approval: number;
|
|
624
|
+
tier_insufficient: number;
|
|
625
|
+
};
|
|
626
|
+
original: {
|
|
627
|
+
allow: number;
|
|
628
|
+
deny: number;
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
interface SimulationSummary {
|
|
632
|
+
policy_file: string;
|
|
633
|
+
log_file: string;
|
|
634
|
+
total_calls: number;
|
|
635
|
+
results: {
|
|
636
|
+
allow: number;
|
|
637
|
+
block: number;
|
|
638
|
+
rate_limited: number;
|
|
639
|
+
require_approval: number;
|
|
640
|
+
tier_insufficient: number;
|
|
641
|
+
};
|
|
642
|
+
original: {
|
|
643
|
+
allow: number;
|
|
644
|
+
deny: number;
|
|
645
|
+
};
|
|
646
|
+
tool_breakdown: SimulationResult[];
|
|
647
|
+
changes: string[];
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Parse a JSONL log file into log entries.
|
|
651
|
+
*/
|
|
652
|
+
declare function parseLogFile(path: string): LogEntry[];
|
|
653
|
+
/**
|
|
654
|
+
* Simulate a policy against a set of log entries.
|
|
655
|
+
* Evaluates each entry against the policy's per-tool rules,
|
|
656
|
+
* including block, rate_limit, min_tier, and require_approval.
|
|
657
|
+
*/
|
|
658
|
+
declare function simulate(entries: LogEntry[], policy: ProtectPolicy, tier?: TrustTier): SimulationSummary;
|
|
659
|
+
/**
|
|
660
|
+
* Format simulation results for terminal output.
|
|
661
|
+
*/
|
|
662
|
+
declare function formatSimulation(summary: SimulationSummary): string;
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* protect-mcp report — compliance report generation
|
|
666
|
+
*
|
|
667
|
+
* Generates structured compliance reports from local log and receipt files.
|
|
668
|
+
* Output as JSON (machine-readable) or Markdown (human-readable, PDF-convertible).
|
|
669
|
+
*
|
|
670
|
+
* Usage:
|
|
671
|
+
* npx protect-mcp report --period 30d --output report.json
|
|
672
|
+
* npx protect-mcp report --period 30d --format md --output report.md
|
|
673
|
+
*/
|
|
674
|
+
interface ComplianceReport {
|
|
675
|
+
generated_at: string;
|
|
676
|
+
period: {
|
|
677
|
+
from: string;
|
|
678
|
+
to: string;
|
|
679
|
+
};
|
|
680
|
+
signing_identity: {
|
|
681
|
+
kid: string;
|
|
682
|
+
issuer: string;
|
|
683
|
+
} | null;
|
|
684
|
+
summary: {
|
|
685
|
+
total_decisions: number;
|
|
686
|
+
allowed: number;
|
|
687
|
+
blocked: number;
|
|
688
|
+
rate_limited: number;
|
|
689
|
+
approval_required: number;
|
|
690
|
+
unique_tools: number;
|
|
691
|
+
unique_tiers: number;
|
|
692
|
+
};
|
|
693
|
+
tool_breakdown: Array<{
|
|
694
|
+
tool: string;
|
|
695
|
+
total: number;
|
|
696
|
+
allowed: number;
|
|
697
|
+
blocked: number;
|
|
698
|
+
rate_limited: number;
|
|
699
|
+
approval_required: number;
|
|
700
|
+
}>;
|
|
701
|
+
policy_changes: Array<{
|
|
702
|
+
at: string;
|
|
703
|
+
policy_digest: string;
|
|
704
|
+
}>;
|
|
705
|
+
verification: {
|
|
706
|
+
receipts_signed: number;
|
|
707
|
+
receipts_unsigned: number;
|
|
708
|
+
verify_command: string;
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Generate a compliance report from local log and receipt files.
|
|
713
|
+
*/
|
|
714
|
+
declare function generateReport(logPath: string, receiptPath: string, periodDays: number): ComplianceReport;
|
|
715
|
+
/**
|
|
716
|
+
* Format a compliance report as Markdown.
|
|
717
|
+
*/
|
|
718
|
+
declare function formatReportMarkdown(report: ComplianceReport): string;
|
|
719
|
+
|
|
573
720
|
/**
|
|
574
721
|
* Agent identity format: sb:agent:{first 32 hex chars of SHA-256(public key bytes)}
|
|
575
722
|
* Example: "sb:agent:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
|
|
@@ -851,4 +998,4 @@ declare function validateManifest(manifest: unknown): string[];
|
|
|
851
998
|
*/
|
|
852
999
|
declare function validateEvidenceReceipt(receipt: unknown): string[];
|
|
853
1000
|
|
|
854
|
-
export { type AdmissionResult, type AgentId, type AgentManifest, type ArenaPayload, type ArenaReceipt, type AttestationPayload, type AttestationReceipt, type AuditBundle, type AuditBundleOptions, type BenchmarkPayload, type BenchmarkReceipt, type BuilderId, type CredentialConfig, type DecisionContext, type DecisionLog, type DisclosureMode, type Ed25519PublicKey, type EvidenceIssuer, type EvidenceReceipt, type EvidenceReceiptBase, type EvidenceSummary, type EvidenceSummaryEntry, type EvidenceType, type ExternalDecision, type ExternalPDPConfig, type IssuerType, type JsonRpcRequest, type JsonRpcResponse, type LeaseCompatibility, type ManifestBuilder, type ManifestCapabilities, type ManifestConfig, type ManifestIdentity, type ManifestPresentation, type ManifestSignature, type ManifestStatus, type PolicyEngineMode, type ProtectConfig, ProtectGateway, type ProtectPolicy, type RateLimit, type RestraintPayload, type RestraintReceipt, type SHA256Hash, type SigningConfig, type TierOverrides, type ToolPolicy, type TrustTier, type WorkPayload, type WorkReceipt, buildDecisionContext, checkRateLimit, collectSignedReceipts, createAuditBundle, evaluateTier, getSignerInfo, getToolPolicy, initSigning, isAgentId, isDisclosureMode, isEvidenceType, isManifestStatus, isSigningEnabled, listCredentialLabels, loadPolicy, meetsMinTier, parseRateLimit, queryExternalPDP, resolveCredential, signDecision, validateCredentials, validateEvidenceReceipt, validateManifest };
|
|
1001
|
+
export { type AdmissionResult, type AgentId, type AgentManifest, type ArenaPayload, type ArenaReceipt, type AttestationPayload, type AttestationReceipt, type AuditBundle, type AuditBundleOptions, type BenchmarkPayload, type BenchmarkReceipt, type BuilderId, type ComplianceReport, type CredentialConfig, type DecisionContext, type DecisionLog, type DisclosureMode, type Ed25519PublicKey, type EvidenceIssuer, type EvidenceReceipt, type EvidenceReceiptBase, type EvidenceSummary, type EvidenceSummaryEntry, type EvidenceType, type ExternalDecision, type ExternalPDPConfig, type IssuerType, type JsonRpcRequest, type JsonRpcResponse, type LeaseCompatibility, type ManifestBuilder, type ManifestCapabilities, type ManifestConfig, type ManifestIdentity, type ManifestPresentation, type ManifestSignature, type ManifestStatus, type PolicyEngineMode, type ProtectConfig, ProtectGateway, type ProtectPolicy, type RateLimit, type RestraintPayload, type RestraintReceipt, type SHA256Hash, type SigningConfig, type SimulationResult, type SimulationSummary, type TierOverrides, type ToolPolicy, type TrustTier, type WorkPayload, type WorkReceipt, buildDecisionContext, checkRateLimit, collectSignedReceipts, createAuditBundle, evaluateTier, formatReportMarkdown, formatSimulation, generateReport, getSignerInfo, getToolPolicy, initSigning, isAgentId, isDisclosureMode, isEvidenceType, isManifestStatus, isSigningEnabled, listCredentialLabels, loadPolicy, meetsMinTier, parseLogFile, parseRateLimit, queryExternalPDP, resolveCredential, signDecision, simulate, validateCredentials, validateEvidenceReceipt, validateManifest };
|
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;
|
|
@@ -522,6 +521,28 @@ function formatRequest(context, format) {
|
|
|
522
521
|
},
|
|
523
522
|
actions: [context.action.operation || "call"]
|
|
524
523
|
};
|
|
524
|
+
case "cedar":
|
|
525
|
+
return {
|
|
526
|
+
principal: {
|
|
527
|
+
type: "Agent",
|
|
528
|
+
id: context.actor.id || "unknown"
|
|
529
|
+
},
|
|
530
|
+
action: {
|
|
531
|
+
type: "Action",
|
|
532
|
+
id: `MCP::Tool::${context.action.operation || "call"}`
|
|
533
|
+
},
|
|
534
|
+
resource: {
|
|
535
|
+
type: "Tool",
|
|
536
|
+
id: context.action.tool
|
|
537
|
+
},
|
|
538
|
+
context: {
|
|
539
|
+
tier: context.actor.tier,
|
|
540
|
+
manifest_hash: context.actor.manifest_hash || null,
|
|
541
|
+
service: context.target.service || "default",
|
|
542
|
+
mode: context.mode,
|
|
543
|
+
credential_ref: context.credential_ref || null
|
|
544
|
+
}
|
|
545
|
+
};
|
|
525
546
|
case "generic":
|
|
526
547
|
default:
|
|
527
548
|
return context;
|
|
@@ -551,6 +572,22 @@ function parseResponse(result, format) {
|
|
|
551
572
|
}
|
|
552
573
|
}
|
|
553
574
|
return { allowed: false, reason: "unrecognized Cerbos response" };
|
|
575
|
+
case "cedar":
|
|
576
|
+
if (typeof result.decision === "string") {
|
|
577
|
+
return {
|
|
578
|
+
allowed: result.decision === "Allow",
|
|
579
|
+
reason: result.decision === "Deny" ? `cedar_deny${result.diagnostics ? ": " + JSON.stringify(result.diagnostics) : ""}` : void 0,
|
|
580
|
+
metadata: result.diagnostics
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (Array.isArray(result.results) && result.results.length > 0) {
|
|
584
|
+
const first = result.results[0];
|
|
585
|
+
return {
|
|
586
|
+
allowed: first.decision === "Allow",
|
|
587
|
+
reason: first.decision === "Deny" ? "cedar_deny" : void 0
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
return { allowed: false, reason: "unrecognized Cedar response" };
|
|
554
591
|
case "generic":
|
|
555
592
|
default:
|
|
556
593
|
return {
|
|
@@ -968,8 +1005,31 @@ var ProtectGateway = class {
|
|
|
968
1005
|
async interceptToolCall(request) {
|
|
969
1006
|
const toolName = request.params?.name || "unknown";
|
|
970
1007
|
const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
|
|
971
|
-
const toolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
972
1008
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
1009
|
+
let resolvedAgentKid = this.admissionResult?.agent_id;
|
|
1010
|
+
let effectiveToolPolicy;
|
|
1011
|
+
if (this.config.multiAgent?.enabled) {
|
|
1012
|
+
const paramKid = request.params?._passport_kid;
|
|
1013
|
+
if (paramKid) resolvedAgentKid = paramKid;
|
|
1014
|
+
const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
|
|
1015
|
+
if (agentOverrides && agentOverrides[toolName]) {
|
|
1016
|
+
effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
|
|
1017
|
+
} else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
|
|
1018
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
|
|
1019
|
+
if (this.config.enforce) {
|
|
1020
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
|
|
1021
|
+
}
|
|
1022
|
+
return null;
|
|
1023
|
+
} else {
|
|
1024
|
+
effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
1025
|
+
}
|
|
1026
|
+
if (this.config.verbose && resolvedAgentKid) {
|
|
1027
|
+
this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
1031
|
+
}
|
|
1032
|
+
const toolPolicy = effectiveToolPolicy;
|
|
973
1033
|
let credentialRef;
|
|
974
1034
|
if (this.config.credentials) {
|
|
975
1035
|
const cred = resolveCredential(toolName, this.config.credentials);
|
|
@@ -1193,6 +1253,298 @@ function collectSignedReceipts(logs) {
|
|
|
1193
1253
|
}).filter((r) => typeof r.signature === "string");
|
|
1194
1254
|
}
|
|
1195
1255
|
|
|
1256
|
+
// src/simulate.ts
|
|
1257
|
+
var import_node_fs6 = require("fs");
|
|
1258
|
+
function parseLogFile(path) {
|
|
1259
|
+
const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
|
|
1260
|
+
const entries = [];
|
|
1261
|
+
for (const line of raw.split("\n")) {
|
|
1262
|
+
const trimmed = line.trim();
|
|
1263
|
+
if (!trimmed) continue;
|
|
1264
|
+
const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
|
|
1265
|
+
try {
|
|
1266
|
+
const parsed = JSON.parse(jsonStr);
|
|
1267
|
+
if (parsed.tool && parsed.decision) {
|
|
1268
|
+
entries.push(parsed);
|
|
1269
|
+
}
|
|
1270
|
+
} catch {
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return entries;
|
|
1274
|
+
}
|
|
1275
|
+
function simulate(entries, policy, tier = "unknown") {
|
|
1276
|
+
const rateLimitStore = /* @__PURE__ */ new Map();
|
|
1277
|
+
const toolResults = /* @__PURE__ */ new Map();
|
|
1278
|
+
const totals = {
|
|
1279
|
+
allow: 0,
|
|
1280
|
+
block: 0,
|
|
1281
|
+
rate_limited: 0,
|
|
1282
|
+
require_approval: 0,
|
|
1283
|
+
tier_insufficient: 0
|
|
1284
|
+
};
|
|
1285
|
+
const originalTotals = { allow: 0, deny: 0 };
|
|
1286
|
+
const changes = [];
|
|
1287
|
+
for (const entry of entries) {
|
|
1288
|
+
const toolName = entry.tool;
|
|
1289
|
+
const toolPolicy = getToolPolicy(toolName, policy);
|
|
1290
|
+
if (entry.decision === "allow") {
|
|
1291
|
+
originalTotals.allow++;
|
|
1292
|
+
} else {
|
|
1293
|
+
originalTotals.deny++;
|
|
1294
|
+
}
|
|
1295
|
+
let newDecision;
|
|
1296
|
+
if (toolPolicy.block) {
|
|
1297
|
+
newDecision = "block";
|
|
1298
|
+
} else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
|
|
1299
|
+
newDecision = "tier_insufficient";
|
|
1300
|
+
} else if (toolPolicy.require_approval) {
|
|
1301
|
+
newDecision = "require_approval";
|
|
1302
|
+
} else if (toolPolicy.rate_limit) {
|
|
1303
|
+
const limit = parseRateLimit(toolPolicy.rate_limit);
|
|
1304
|
+
const result = checkRateLimit(toolName, limit, rateLimitStore);
|
|
1305
|
+
newDecision = result.allowed ? "allow" : "rate_limited";
|
|
1306
|
+
} else {
|
|
1307
|
+
newDecision = "allow";
|
|
1308
|
+
}
|
|
1309
|
+
totals[newDecision]++;
|
|
1310
|
+
if (!toolResults.has(toolName)) {
|
|
1311
|
+
toolResults.set(toolName, {
|
|
1312
|
+
tool: toolName,
|
|
1313
|
+
calls: 0,
|
|
1314
|
+
results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
|
|
1315
|
+
original: { allow: 0, deny: 0 }
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
const tr = toolResults.get(toolName);
|
|
1319
|
+
tr.calls++;
|
|
1320
|
+
tr.results[newDecision]++;
|
|
1321
|
+
if (entry.decision === "allow") {
|
|
1322
|
+
tr.original.allow++;
|
|
1323
|
+
} else {
|
|
1324
|
+
tr.original.deny++;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
for (const [tool, result] of toolResults) {
|
|
1328
|
+
const wasAllBlocked = result.original.allow === 0;
|
|
1329
|
+
const nowAllBlocked = result.results.allow === 0;
|
|
1330
|
+
const wasAllAllowed = result.original.deny === 0;
|
|
1331
|
+
if (wasAllAllowed && result.results.block > 0) {
|
|
1332
|
+
changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
|
|
1333
|
+
}
|
|
1334
|
+
if (wasAllAllowed && result.results.rate_limited > 0) {
|
|
1335
|
+
changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
|
|
1336
|
+
}
|
|
1337
|
+
if (wasAllAllowed && result.results.require_approval > 0) {
|
|
1338
|
+
changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
|
|
1339
|
+
}
|
|
1340
|
+
if (wasAllAllowed && result.results.tier_insufficient > 0) {
|
|
1341
|
+
changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
|
|
1342
|
+
}
|
|
1343
|
+
if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
|
|
1344
|
+
changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return {
|
|
1348
|
+
policy_file: "",
|
|
1349
|
+
log_file: "",
|
|
1350
|
+
total_calls: entries.length,
|
|
1351
|
+
results: totals,
|
|
1352
|
+
original: originalTotals,
|
|
1353
|
+
tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
|
|
1354
|
+
changes
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
function formatSimulation(summary) {
|
|
1358
|
+
const lines = [];
|
|
1359
|
+
lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
|
|
1360
|
+
`);
|
|
1361
|
+
const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
|
|
1362
|
+
for (const tr of summary.tool_breakdown) {
|
|
1363
|
+
const parts = [];
|
|
1364
|
+
if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
|
|
1365
|
+
if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
|
|
1366
|
+
if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
|
|
1367
|
+
if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
|
|
1368
|
+
if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
|
|
1369
|
+
const originalParts = [];
|
|
1370
|
+
if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
|
|
1371
|
+
if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
|
|
1372
|
+
lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
|
|
1373
|
+
}
|
|
1374
|
+
lines.push("");
|
|
1375
|
+
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`);
|
|
1376
|
+
lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
|
|
1377
|
+
if (summary.changes.length > 0) {
|
|
1378
|
+
lines.push("");
|
|
1379
|
+
lines.push("Changes:");
|
|
1380
|
+
for (const change of summary.changes) {
|
|
1381
|
+
lines.push(` \u2022 ${change}`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return lines.join("\n");
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// src/report.ts
|
|
1388
|
+
var import_node_fs7 = require("fs");
|
|
1389
|
+
function generateReport(logPath, receiptPath, periodDays) {
|
|
1390
|
+
const now = /* @__PURE__ */ new Date();
|
|
1391
|
+
const from = new Date(now.getTime() - periodDays * 864e5);
|
|
1392
|
+
const entries = [];
|
|
1393
|
+
if ((0, import_node_fs7.existsSync)(logPath)) {
|
|
1394
|
+
const raw = (0, import_node_fs7.readFileSync)(logPath, "utf-8");
|
|
1395
|
+
for (const line of raw.split("\n")) {
|
|
1396
|
+
const trimmed = line.trim();
|
|
1397
|
+
if (!trimmed) continue;
|
|
1398
|
+
const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
|
|
1399
|
+
try {
|
|
1400
|
+
const parsed = JSON.parse(jsonStr);
|
|
1401
|
+
if (parsed.tool && parsed.decision && parsed.timestamp) {
|
|
1402
|
+
const entryTime = typeof parsed.timestamp === "number" && parsed.timestamp > 1e12 ? parsed.timestamp : parsed.timestamp * 1e3;
|
|
1403
|
+
if (entryTime >= from.getTime()) {
|
|
1404
|
+
entries.push(parsed);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
} catch {
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
let receiptsSigned = 0;
|
|
1412
|
+
let signerKid = "";
|
|
1413
|
+
let signerIssuer = "";
|
|
1414
|
+
if ((0, import_node_fs7.existsSync)(receiptPath)) {
|
|
1415
|
+
const raw = (0, import_node_fs7.readFileSync)(receiptPath, "utf-8");
|
|
1416
|
+
for (const line of raw.split("\n")) {
|
|
1417
|
+
const trimmed = line.trim();
|
|
1418
|
+
if (!trimmed) continue;
|
|
1419
|
+
try {
|
|
1420
|
+
const parsed = JSON.parse(trimmed);
|
|
1421
|
+
if (parsed.signature) {
|
|
1422
|
+
receiptsSigned++;
|
|
1423
|
+
if (parsed.kid && !signerKid) signerKid = parsed.kid;
|
|
1424
|
+
if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
|
|
1425
|
+
}
|
|
1426
|
+
} catch {
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
1431
|
+
const tiers = /* @__PURE__ */ new Set();
|
|
1432
|
+
const policyDigests = /* @__PURE__ */ new Map();
|
|
1433
|
+
let allowed = 0;
|
|
1434
|
+
let blocked = 0;
|
|
1435
|
+
let rateLimited = 0;
|
|
1436
|
+
let approvalRequired = 0;
|
|
1437
|
+
for (const entry of entries) {
|
|
1438
|
+
const tool = entry.tool;
|
|
1439
|
+
if (!toolMap.has(tool)) {
|
|
1440
|
+
toolMap.set(tool, { total: 0, allowed: 0, blocked: 0, rate_limited: 0, approval_required: 0 });
|
|
1441
|
+
}
|
|
1442
|
+
const tm = toolMap.get(tool);
|
|
1443
|
+
tm.total++;
|
|
1444
|
+
if (entry.decision === "allow") {
|
|
1445
|
+
allowed++;
|
|
1446
|
+
tm.allowed++;
|
|
1447
|
+
} else if (entry.decision === "deny" && entry.reason_code === "rate_limit_exceeded") {
|
|
1448
|
+
rateLimited++;
|
|
1449
|
+
tm.rate_limited++;
|
|
1450
|
+
} else if (entry.decision === "deny" && entry.reason_code === "require_approval") {
|
|
1451
|
+
approvalRequired++;
|
|
1452
|
+
tm.approval_required++;
|
|
1453
|
+
} else {
|
|
1454
|
+
blocked++;
|
|
1455
|
+
tm.blocked++;
|
|
1456
|
+
}
|
|
1457
|
+
if (entry.tier) tiers.add(entry.tier);
|
|
1458
|
+
if (entry.policy_digest && !policyDigests.has(entry.policy_digest)) {
|
|
1459
|
+
policyDigests.set(entry.policy_digest, new Date(entry.timestamp).toISOString());
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const policyChanges = Array.from(policyDigests.entries()).map(([digest, at]) => ({
|
|
1463
|
+
at,
|
|
1464
|
+
policy_digest: digest
|
|
1465
|
+
})).sort((a, b) => a.at.localeCompare(b.at));
|
|
1466
|
+
return {
|
|
1467
|
+
generated_at: now.toISOString(),
|
|
1468
|
+
period: { from: from.toISOString(), to: now.toISOString() },
|
|
1469
|
+
signing_identity: signerKid ? { kid: signerKid, issuer: signerIssuer } : null,
|
|
1470
|
+
summary: {
|
|
1471
|
+
total_decisions: entries.length,
|
|
1472
|
+
allowed,
|
|
1473
|
+
blocked,
|
|
1474
|
+
rate_limited: rateLimited,
|
|
1475
|
+
approval_required: approvalRequired,
|
|
1476
|
+
unique_tools: toolMap.size,
|
|
1477
|
+
unique_tiers: tiers.size
|
|
1478
|
+
},
|
|
1479
|
+
tool_breakdown: Array.from(toolMap.entries()).map(([tool, stats]) => ({ tool, ...stats })).sort((a, b) => b.total - a.total),
|
|
1480
|
+
policy_changes: policyChanges,
|
|
1481
|
+
verification: {
|
|
1482
|
+
receipts_signed: receiptsSigned,
|
|
1483
|
+
receipts_unsigned: entries.length - receiptsSigned,
|
|
1484
|
+
verify_command: "npx @veritasacta/verify audit-bundle.json --bundle"
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function formatReportMarkdown(report) {
|
|
1489
|
+
const lines = [];
|
|
1490
|
+
lines.push("# ScopeBlind Compliance Report");
|
|
1491
|
+
lines.push("");
|
|
1492
|
+
lines.push(`**Generated:** ${report.generated_at}`);
|
|
1493
|
+
lines.push(`**Period:** ${report.period.from.split("T")[0]} to ${report.period.to.split("T")[0]}`);
|
|
1494
|
+
if (report.signing_identity) {
|
|
1495
|
+
lines.push(`**Signing identity:** kid \`${report.signing_identity.kid}\`, issuer \`${report.signing_identity.issuer}\``);
|
|
1496
|
+
}
|
|
1497
|
+
lines.push("");
|
|
1498
|
+
lines.push("## Summary");
|
|
1499
|
+
lines.push("");
|
|
1500
|
+
lines.push(`| Metric | Value |`);
|
|
1501
|
+
lines.push(`|--------|-------|`);
|
|
1502
|
+
lines.push(`| Total decisions | ${report.summary.total_decisions} |`);
|
|
1503
|
+
lines.push(`| Allowed | ${report.summary.allowed} |`);
|
|
1504
|
+
lines.push(`| Blocked | ${report.summary.blocked} |`);
|
|
1505
|
+
lines.push(`| Rate-limited | ${report.summary.rate_limited} |`);
|
|
1506
|
+
lines.push(`| Approval required | ${report.summary.approval_required} |`);
|
|
1507
|
+
lines.push(`| Unique tools | ${report.summary.unique_tools} |`);
|
|
1508
|
+
lines.push(`| Unique tiers | ${report.summary.unique_tiers} |`);
|
|
1509
|
+
lines.push("");
|
|
1510
|
+
if (report.tool_breakdown.length > 0) {
|
|
1511
|
+
lines.push("## Tool Breakdown");
|
|
1512
|
+
lines.push("");
|
|
1513
|
+
lines.push("| Tool | Total | Allowed | Blocked | Rate-limited | Approval |");
|
|
1514
|
+
lines.push("|------|-------|---------|---------|--------------|----------|");
|
|
1515
|
+
for (const t of report.tool_breakdown) {
|
|
1516
|
+
lines.push(`| \`${t.tool}\` | ${t.total} | ${t.allowed} | ${t.blocked} | ${t.rate_limited} | ${t.approval_required} |`);
|
|
1517
|
+
}
|
|
1518
|
+
lines.push("");
|
|
1519
|
+
}
|
|
1520
|
+
if (report.policy_changes.length > 0) {
|
|
1521
|
+
lines.push("## Policy History");
|
|
1522
|
+
lines.push("");
|
|
1523
|
+
lines.push("| Timestamp | Policy Digest |");
|
|
1524
|
+
lines.push("|-----------|--------------|");
|
|
1525
|
+
for (const pc of report.policy_changes) {
|
|
1526
|
+
lines.push(`| ${pc.at} | \`${pc.policy_digest}\` |`);
|
|
1527
|
+
}
|
|
1528
|
+
lines.push("");
|
|
1529
|
+
}
|
|
1530
|
+
lines.push("## Verification");
|
|
1531
|
+
lines.push("");
|
|
1532
|
+
lines.push(`- Receipts signed: **${report.verification.receipts_signed}**`);
|
|
1533
|
+
lines.push(`- Receipts unsigned: **${report.verification.receipts_unsigned}**`);
|
|
1534
|
+
lines.push("");
|
|
1535
|
+
lines.push("Verify the audit bundle:");
|
|
1536
|
+
lines.push("");
|
|
1537
|
+
lines.push("```bash");
|
|
1538
|
+
lines.push(report.verification.verify_command);
|
|
1539
|
+
lines.push("```");
|
|
1540
|
+
lines.push("");
|
|
1541
|
+
lines.push("The verifier is MIT-licensed and works offline. No ScopeBlind account required.");
|
|
1542
|
+
lines.push("");
|
|
1543
|
+
lines.push("---");
|
|
1544
|
+
lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
|
|
1545
|
+
return lines.join("\n");
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1196
1548
|
// src/manifest.ts
|
|
1197
1549
|
function isAgentId(s) {
|
|
1198
1550
|
return /^sb:agent:[a-f0-9]{32}$/.test(s);
|
|
@@ -1355,6 +1707,9 @@ function validateEvidenceReceipt(receipt) {
|
|
|
1355
1707
|
collectSignedReceipts,
|
|
1356
1708
|
createAuditBundle,
|
|
1357
1709
|
evaluateTier,
|
|
1710
|
+
formatReportMarkdown,
|
|
1711
|
+
formatSimulation,
|
|
1712
|
+
generateReport,
|
|
1358
1713
|
getSignerInfo,
|
|
1359
1714
|
getToolPolicy,
|
|
1360
1715
|
initSigning,
|
|
@@ -1366,10 +1721,12 @@ function validateEvidenceReceipt(receipt) {
|
|
|
1366
1721
|
listCredentialLabels,
|
|
1367
1722
|
loadPolicy,
|
|
1368
1723
|
meetsMinTier,
|
|
1724
|
+
parseLogFile,
|
|
1369
1725
|
parseRateLimit,
|
|
1370
1726
|
queryExternalPDP,
|
|
1371
1727
|
resolveCredential,
|
|
1372
1728
|
signDecision,
|
|
1729
|
+
simulate,
|
|
1373
1730
|
validateCredentials,
|
|
1374
1731
|
validateEvidenceReceipt,
|
|
1375
1732
|
validateManifest
|