Skip to content

Commit f728e47

Browse files
linus131313claude
andcommitted
Add breakpoints (B1-B6), policy engine, and framework mappings
Operationalises the six control breakpoints from Table 6 of the paper plus the YAML policy engine and the framework-clause mappings that turn an attestation into an auditable artefact. - breakpoints: b1_change, b2_thirdparty, b3_dlp, b4_privilege, b5_audit, b6_capability_state. Each returns a CheckResult with severity pass/info/warn/block, structured evidence, and paper-aligned titles - policy.engine: Policy (YAML) + PolicyReport; Policy.load() reads the document; evaluate() runs all six checks with the policy context - policies/: default.yaml (balanced), developer.yaml (TCS <= 20), restricted.yaml (analyst, TCS <= 10 and execute requires approval) - mappings/: ISO 42001 Annex A, NIST AI 600-1, EU AI Act YAMLs mapping each B-check to the concrete framework clauses it addresses - CLI: `mcp-gov check`, `mcp-gov mappings list|show` 51/51 tests green, mypy strict clean, ruff clean, coverage 82%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9258090 commit f728e47

20 files changed

Lines changed: 1150 additions & 0 deletions

policies/default.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Balanced default policy. Warns on deviations but only blocks the
2+
# highest-privilege mis-configurations. Intended as a starting point
3+
# for teams adopting the kit.
4+
version: 1
5+
name: default
6+
max_tcs: 30
7+
warn_tcs: 15
8+
max_third_party: 6
9+
third_party_allowlist:
10+
- "@modelcontextprotocol/server-github"
11+
- "@modelcontextprotocol/server-filesystem"
12+
- "@modelcontextprotocol/server-sqlite"
13+
- "@modelcontextprotocol/server-fetch"
14+
- "shell-mcp"
15+
- "docker-mcp"
16+
require_approval_for_execute: false
17+
require_approval_for_changes: false
18+
allow_exfil_paths: false

policies/developer.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Developer role: full git/shell workflow permitted, strict cap.
2+
version: 1
3+
name: developer
4+
max_tcs: 20
5+
warn_tcs: 14
6+
max_third_party: 5
7+
third_party_allowlist:
8+
- "@modelcontextprotocol/server-github"
9+
- "@modelcontextprotocol/server-filesystem"
10+
- "shell-mcp"
11+
- "docker-mcp"
12+
require_approval_for_execute: false
13+
require_approval_for_changes: false
14+
allow_exfil_paths: false

policies/restricted.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Analyst / data-science role: read and bounded write, no execute.
2+
version: 1
3+
name: restricted
4+
max_tcs: 10
5+
warn_tcs: 5
6+
max_third_party: 3
7+
third_party_allowlist:
8+
- "@modelcontextprotocol/server-filesystem"
9+
- "@modelcontextprotocol/server-sqlite"
10+
- "@modelcontextprotocol/server-fetch"
11+
require_approval_for_execute: true
12+
require_approval_for_changes: true
13+
allow_exfil_paths: false
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Control-breakpoint checks (B1–B6 from Table 6 of the paper).
2+
3+
Each check operationalises one control family that the paper identifies
4+
as silently broken under an MCP-native agent architecture:
5+
6+
* B1 — change management (ISO 27001 A.8.32, COBIT BAI06)
7+
* B2 — third-party risk (ISO 27001 A.5.19, NIST SP 800-161)
8+
* B3 — data loss prevention (ISO 27001 A.8.12)
9+
* B4 — privileged access (ISO 27001 A.8.2, NIST AC-6)
10+
* B5 — audit and monitoring (ISO 27001 A.8.15, NIST AU family)
11+
* B6 — AI-specific capability state (ISO 42001 A.6, NIST AI 600-1)
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from mcp_governance_kit.breakpoints.b1_change import b1_change
17+
from mcp_governance_kit.breakpoints.b2_thirdparty import b2_thirdparty
18+
from mcp_governance_kit.breakpoints.b3_dlp import b3_dlp
19+
from mcp_governance_kit.breakpoints.b4_privilege import b4_privilege
20+
from mcp_governance_kit.breakpoints.b5_audit import b5_audit
21+
from mcp_governance_kit.breakpoints.b6_capability_state import b6_capability_state
22+
from mcp_governance_kit.breakpoints.base import CheckResult, Severity
23+
24+
ALL_CHECKS = ("B1", "B2", "B3", "B4", "B5", "B6")
25+
26+
__all__ = [
27+
"ALL_CHECKS",
28+
"CheckResult",
29+
"Severity",
30+
"b1_change",
31+
"b2_thirdparty",
32+
"b3_dlp",
33+
"b4_privilege",
34+
"b5_audit",
35+
"b6_capability_state",
36+
]
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""B1 — change management.
2+
3+
Compares two attestations of the same host. Any added, removed, or
4+
reclassified tool is a change that would, under traditional change
5+
management, generate a change record. The check is WARN if any change
6+
is present and the ``changes_approved`` flag is not set; BLOCK if
7+
``require_approval`` is enabled in the policy context and the flag is
8+
not set. PASS otherwise.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from mcp_governance_kit.attest.schema import Attestation, ToolRecord
14+
from mcp_governance_kit.breakpoints.base import CheckResult, Severity
15+
16+
17+
def _tool_key(t: ToolRecord) -> str:
18+
return f"{t.name}@{t.server.identity}"
19+
20+
21+
def b1_change(
22+
previous: Attestation | None,
23+
current: Attestation,
24+
*,
25+
changes_approved: bool = False,
26+
require_approval: bool = False,
27+
) -> CheckResult:
28+
"""Compare two attestations and surface the change set.
29+
30+
If ``previous`` is ``None`` the check is considered INFO (first-time
31+
attestation, no change to compare against).
32+
"""
33+
if previous is None:
34+
return CheckResult(
35+
check_id="B1",
36+
title="Change management",
37+
severity=Severity.INFO,
38+
summary="No prior attestation supplied; change set cannot be computed.",
39+
)
40+
41+
prev_by_key = {_tool_key(t): t for t in previous.tools}
42+
curr_by_key = {_tool_key(t): t for t in current.tools}
43+
44+
added = sorted(set(curr_by_key) - set(prev_by_key))
45+
removed = sorted(set(prev_by_key) - set(curr_by_key))
46+
reclassified: list[str] = []
47+
for key in set(prev_by_key) & set(curr_by_key):
48+
p, c = prev_by_key[key], curr_by_key[key]
49+
if (p.reach, p.action, p.server.third_party) != (c.reach, c.action, c.server.third_party):
50+
reclassified.append(key)
51+
reclassified.sort()
52+
53+
total_changes = len(added) + len(removed) + len(reclassified)
54+
tcs_delta = current.tcs.value - previous.tcs.value
55+
56+
if total_changes == 0:
57+
return CheckResult(
58+
check_id="B1",
59+
title="Change management",
60+
severity=Severity.PASS,
61+
summary="Tool graph unchanged since previous attestation.",
62+
evidence={"tcs_delta": tcs_delta},
63+
)
64+
65+
details = (
66+
[f"+ {k}" for k in added] + [f"- {k}" for k in removed] + [f"~ {k}" for k in reclassified]
67+
)
68+
69+
if changes_approved:
70+
sev = Severity.INFO
71+
msg = f"{total_changes} approved change(s) since previous attestation."
72+
elif require_approval:
73+
sev = Severity.BLOCK
74+
msg = f"{total_changes} unapproved change(s) detected. Policy requires approval for B1."
75+
else:
76+
sev = Severity.WARN
77+
msg = f"{total_changes} change(s) detected without change record."
78+
79+
return CheckResult(
80+
check_id="B1",
81+
title="Change management",
82+
severity=sev,
83+
summary=msg,
84+
details=details,
85+
evidence={
86+
"added": len(added),
87+
"removed": len(removed),
88+
"reclassified": len(reclassified),
89+
"tcs_delta": tcs_delta,
90+
},
91+
)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""B2 — third-party risk.
2+
3+
Enumerates the third-party servers bound to the host and cross-checks
4+
them against an allow-list. Servers that are not on the list are WARN
5+
(INFO if an explicit exception tag is supplied); exceeding a configured
6+
max count is BLOCK.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from collections.abc import Iterable
12+
13+
from mcp_governance_kit.attest.schema import Attestation
14+
from mcp_governance_kit.breakpoints.base import CheckResult, Severity
15+
16+
17+
def b2_thirdparty(
18+
attestation: Attestation,
19+
*,
20+
allowlist: Iterable[str] = (),
21+
max_third_party: int | None = None,
22+
) -> CheckResult:
23+
"""Check third-party server exposure."""
24+
allowed = set(allowlist)
25+
third_party = [t for t in attestation.tools if t.server.third_party]
26+
unapproved = sorted({t.server.identity for t in third_party} - allowed)
27+
28+
if max_third_party is not None and len(third_party) > max_third_party:
29+
return CheckResult(
30+
check_id="B2",
31+
title="Third-party risk",
32+
severity=Severity.BLOCK,
33+
summary=(
34+
f"{len(third_party)} third-party server(s), "
35+
f"exceeds policy maximum of {max_third_party}."
36+
),
37+
details=[t.server.identity for t in third_party],
38+
evidence={"third_party_count": len(third_party)},
39+
)
40+
41+
if unapproved:
42+
return CheckResult(
43+
check_id="B2",
44+
title="Third-party risk",
45+
severity=Severity.WARN,
46+
summary=f"{len(unapproved)} third-party server(s) not on allow-list.",
47+
details=unapproved,
48+
evidence={
49+
"unapproved_count": len(unapproved),
50+
"third_party_count": len(third_party),
51+
},
52+
)
53+
54+
return CheckResult(
55+
check_id="B2",
56+
title="Third-party risk",
57+
severity=Severity.PASS,
58+
summary=f"All {len(third_party)} third-party server(s) are on the allow-list.",
59+
evidence={"third_party_count": len(third_party)},
60+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""B3 — data loss prevention.
2+
3+
Flags tool-graph compositions where data can move from local disk to a
4+
network-reaching write tool without crossing a DLP egress point. The
5+
simplest proxy: any tool graph that contains both a local-read/write
6+
tool and a network-write tool is a potential exfil path.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from mcp_governance_kit.attest.schema import Attestation
12+
from mcp_governance_kit.breakpoints.base import CheckResult, Severity
13+
from mcp_governance_kit.tcs.models import Action, Reach
14+
15+
16+
def b3_dlp(attestation: Attestation, *, allow_exfil_paths: bool = False) -> CheckResult:
17+
"""Detect potential data-exfiltration paths in the tool graph."""
18+
local_access = [
19+
t
20+
for t in attestation.tools
21+
if t.reach is Reach.LOCAL and t.action in (Action.READ, Action.WRITE, Action.EXECUTE)
22+
]
23+
net_write = [
24+
t
25+
for t in attestation.tools
26+
if t.reach is Reach.NETWORK and t.action in (Action.WRITE, Action.EXECUTE)
27+
]
28+
29+
if not (local_access and net_write):
30+
return CheckResult(
31+
check_id="B3",
32+
title="Data loss prevention",
33+
severity=Severity.PASS,
34+
summary="No local-to-network exfil path in tool graph.",
35+
)
36+
37+
paths = [
38+
f"{lo.server.identity} -> {no.server.identity}" for lo in local_access for no in net_write
39+
]
40+
sev = Severity.INFO if allow_exfil_paths else Severity.WARN
41+
return CheckResult(
42+
check_id="B3",
43+
title="Data loss prevention",
44+
severity=sev,
45+
summary=(
46+
f"{len(local_access)} local-access + {len(net_write)} network-write tool(s) "
47+
f"= {len(paths)} potential exfil path(s)."
48+
),
49+
details=paths[:20],
50+
evidence={
51+
"local_access_count": len(local_access),
52+
"net_write_count": len(net_write),
53+
"path_count": len(paths),
54+
},
55+
)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""B4 — privileged access.
2+
3+
Two sub-checks:
4+
5+
1. TCS threshold. If the attestation's TCS exceeds the configured
6+
threshold, the check escalates to WARN or BLOCK.
7+
2. Execute capability. Any tool with ``action=execute`` is flagged;
8+
network+execute combinations are the strictest and go to BLOCK when
9+
``require_approval_for_execute`` is enabled.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from mcp_governance_kit.attest.schema import Attestation
15+
from mcp_governance_kit.breakpoints.base import CheckResult, Severity
16+
from mcp_governance_kit.tcs.models import Action, Reach
17+
18+
19+
def b4_privilege(
20+
attestation: Attestation,
21+
*,
22+
max_tcs: float | None = None,
23+
warn_tcs: float | None = None,
24+
require_approval_for_execute: bool = False,
25+
execute_approved: bool = False,
26+
) -> CheckResult:
27+
"""Evaluate privileged-access exposure."""
28+
tcs_value = attestation.tcs.value
29+
execute_tools = [t for t in attestation.tools if t.action is Action.EXECUTE]
30+
net_execute_tools = [t for t in execute_tools if t.reach is Reach.NETWORK]
31+
32+
details: list[str] = []
33+
evidence: dict[str, str | int | float | bool] = {
34+
"tcs": tcs_value,
35+
"execute_tools": len(execute_tools),
36+
"network_execute_tools": len(net_execute_tools),
37+
}
38+
39+
if max_tcs is not None and tcs_value > max_tcs:
40+
details.extend(f"execute: {t.server.identity}" for t in execute_tools)
41+
return CheckResult(
42+
check_id="B4",
43+
title="Privileged access",
44+
severity=Severity.BLOCK,
45+
summary=f"TCS {tcs_value} exceeds policy maximum {max_tcs}.",
46+
details=details,
47+
evidence=evidence,
48+
)
49+
50+
if require_approval_for_execute and net_execute_tools and not execute_approved:
51+
return CheckResult(
52+
check_id="B4",
53+
title="Privileged access",
54+
severity=Severity.BLOCK,
55+
summary=(
56+
f"{len(net_execute_tools)} network-reaching execute tool(s) require approval."
57+
),
58+
details=[t.server.identity for t in net_execute_tools],
59+
evidence=evidence,
60+
)
61+
62+
if warn_tcs is not None and tcs_value > warn_tcs:
63+
return CheckResult(
64+
check_id="B4",
65+
title="Privileged access",
66+
severity=Severity.WARN,
67+
summary=f"TCS {tcs_value} exceeds advisory threshold {warn_tcs}.",
68+
evidence=evidence,
69+
)
70+
71+
if execute_tools:
72+
return CheckResult(
73+
check_id="B4",
74+
title="Privileged access",
75+
severity=Severity.INFO,
76+
summary=f"{len(execute_tools)} execute-capable tool(s) bound; within thresholds.",
77+
details=[t.server.identity for t in execute_tools],
78+
evidence=evidence,
79+
)
80+
81+
return CheckResult(
82+
check_id="B4",
83+
title="Privileged access",
84+
severity=Severity.PASS,
85+
summary=f"No execute-capable tools; TCS {tcs_value} within thresholds.",
86+
evidence=evidence,
87+
)

0 commit comments

Comments
 (0)