mcp-attest 0.1.0__tar.gz
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.
- mcp_attest-0.1.0/LICENSE +21 -0
- mcp_attest-0.1.0/PKG-INFO +239 -0
- mcp_attest-0.1.0/README.md +217 -0
- mcp_attest-0.1.0/pyproject.toml +34 -0
- mcp_attest-0.1.0/setup.cfg +4 -0
- mcp_attest-0.1.0/src/mcp_attest/__init__.py +24 -0
- mcp_attest-0.1.0/src/mcp_attest/__main__.py +4 -0
- mcp_attest-0.1.0/src/mcp_attest/checks.py +391 -0
- mcp_attest-0.1.0/src/mcp_attest/cli.py +179 -0
- mcp_attest-0.1.0/src/mcp_attest/policy.py +236 -0
- mcp_attest-0.1.0/src/mcp_attest/receipts.py +112 -0
- mcp_attest-0.1.0/src/mcp_attest/shadow.py +38 -0
- mcp_attest-0.1.0/src/mcp_attest/trace.py +153 -0
- mcp_attest-0.1.0/src/mcp_attest.egg-info/PKG-INFO +239 -0
- mcp_attest-0.1.0/src/mcp_attest.egg-info/SOURCES.txt +17 -0
- mcp_attest-0.1.0/src/mcp_attest.egg-info/dependency_links.txt +1 -0
- mcp_attest-0.1.0/src/mcp_attest.egg-info/entry_points.txt +2 -0
- mcp_attest-0.1.0/src/mcp_attest.egg-info/top_level.txt +1 -0
- mcp_attest-0.1.0/tests/test_mcp_attest.py +131 -0
mcp_attest-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Balanov.PRO
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-attest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Conformance and attestation harness for MCP agent access control: prove that a policy actually enforces, detect shadow MCP paths, and emit tamper-evident receipts.
|
|
5
|
+
Author: Balanov.PRO
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Balanov/mcp-attest
|
|
8
|
+
Project-URL: Source, https://github.com/Balanov/mcp-attest
|
|
9
|
+
Project-URL: Issues, https://github.com/Balanov/mcp-attest/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,security,policy,audit,attestation,agents
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Information Technology
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# mcp-attest
|
|
24
|
+
|
|
25
|
+
**Prove that your MCP policy actually enforces.**
|
|
26
|
+
|
|
27
|
+
`mcp-attest` is a conformance and attestation harness for agent access control.
|
|
28
|
+
It takes the policy you *wrote* and the trace of what *actually happened*, and
|
|
29
|
+
reports every point where they disagree — including the failure modes that
|
|
30
|
+
gateways accept silently: a rule that was registered but never enforced, a
|
|
31
|
+
tool that is hidden from `tools/list` but still callable, a client that reaches
|
|
32
|
+
a server outside the gateway, and a tool definition that changed after it was
|
|
33
|
+
approved.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pipx install mcp-attest
|
|
37
|
+
attest init
|
|
38
|
+
attest check --policy policy.json --trace trace.jsonl
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
!! [CRITICAL] C6 Policy denies 'shell__exec' but the gateway allowed it
|
|
43
|
+
Local evaluation of the policy says deny (tool is on the deny list); the
|
|
44
|
+
trace records allow ('argument-conditioned rule matched'). This is the
|
|
45
|
+
'policy accepted but not enforced' failure mode — a rule that is
|
|
46
|
+
registered but never applied.
|
|
47
|
+
fix: Replay the call against the gateway with the same arguments and fix
|
|
48
|
+
the rule binding, then add this call as a regression case in CI.
|
|
49
|
+
|
|
50
|
+
!! [CRITICAL] C9 Tool 'fs__read_file' changed after it was listed
|
|
51
|
+
The description hash of an already-approved tool changed between two
|
|
52
|
+
listings. This is the shape of a rug pull: the tool was harmless at
|
|
53
|
+
approval time.
|
|
54
|
+
|
|
55
|
+
summary: critical=7, high=6
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Exit code is `1` when a blocking finding exists, so it works as a CI gate
|
|
59
|
+
without a wrapper script. The session above is `examples/session.jsonl` — run
|
|
60
|
+
`attest check --policy examples/policy.json --trace examples/session.jsonl` to
|
|
61
|
+
reproduce it.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Why this exists
|
|
66
|
+
|
|
67
|
+
Gateways, policy engines and allowlists for Model Context Protocol are now
|
|
68
|
+
everywhere — Docker MCP Gateway, agentgateway, Bifrost, Runlayer, Lasso, Obot,
|
|
69
|
+
and GitHub's own `gh-aw-mcpg`. The policy language is not the problem.
|
|
70
|
+
|
|
71
|
+
The problem is that **nobody verifies the policy is enforced.** In a detailed
|
|
72
|
+
evaluation of agentgateway published in October 2026, an argument-conditioned
|
|
73
|
+
policy was "reported as accepted and still did not enforce what it said", and
|
|
74
|
+
the conclusion was blunt: "turning a policy on is not the end of the job — send
|
|
75
|
+
real calls and check."
|
|
76
|
+
|
|
77
|
+
The same evaluation found policies that were accepted and had no effect at
|
|
78
|
+
all. The State of MCP Security 2026 survey describes the structural gap:
|
|
79
|
+
"Gateways only protect the servers they route. An agent calling an MCP server
|
|
80
|
+
outside the gateway is invisible," and "Most gateways also do not do deep
|
|
81
|
+
content inspection of responses. The gateway enforces who and where, not what."
|
|
82
|
+
|
|
83
|
+
Meanwhile the threat volume is not theoretical: more than 40 CVEs against MCP
|
|
84
|
+
implementations were disclosed in the first four months of 2026, an estimated
|
|
85
|
+
200,000 servers are exposed, 38% of scanned MCP servers had no authentication
|
|
86
|
+
on critical endpoints, and in April 2026 researchers hijacked Claude Code, Gemini
|
|
87
|
+
CLI and GitHub Copilot through malicious pull-request titles — with bug bounties
|
|
88
|
+
paid by the vendors.
|
|
89
|
+
|
|
90
|
+
And the buyers of this category are being told the same thing. The US DoD's
|
|
91
|
+
Center for Security and Information Technology wrote that "without these
|
|
92
|
+
traceable audit logs, incident response and accountability become significantly
|
|
93
|
+
more difficult, especially in multi-tenant environments."
|
|
94
|
+
|
|
95
|
+
So the missing primitive is small, sharp and useful: **a harness that takes a
|
|
96
|
+
policy and a trace and tells you, with evidence, whether your control plane
|
|
97
|
+
does what you wrote down.**
|
|
98
|
+
|
|
99
|
+
`mcp-attest` is that harness. It is not a gateway, it does not sit on the hot
|
|
100
|
+
path, and it does not need to be trusted with your traffic — it reads what
|
|
101
|
+
already happened.
|
|
102
|
+
|
|
103
|
+
## What it checks
|
|
104
|
+
|
|
105
|
+
| ID | Rule | The failure it catches |
|
|
106
|
+
|----|------|------------------------|
|
|
107
|
+
| C1 | default-deny | An unset allowlist silently meaning "allow everything" |
|
|
108
|
+
| C2 | list/call parity | A tool hidden from `tools/list` but callable by name |
|
|
109
|
+
| C3 | narrowing only | A per-request control widening a client's grant |
|
|
110
|
+
| C4 | credential separation | The client's token replayed upstream verbatim |
|
|
111
|
+
| C5 | fail closed | A call allowed because the policy check errored |
|
|
112
|
+
| C6 | enforcement parity | The policy says deny, the gateway allowed it |
|
|
113
|
+
| C7 | shadow MCP | A server the agent can reach outside the gateway |
|
|
114
|
+
| C8 | receipt chain | Allowed calls with no verifiable receipt, or a broken chain |
|
|
115
|
+
| C9 | description integrity | A tool definition changed after approval (rug pull) |
|
|
116
|
+
|
|
117
|
+
C6 is the one that matters most, and it is the one no existing tool does: it
|
|
118
|
+
re-evaluates your policy locally against every recorded call and flags any call
|
|
119
|
+
whose observed decision disagrees with the policy. That catches the
|
|
120
|
+
"accepted but not enforced" class directly.
|
|
121
|
+
|
|
122
|
+
## Install
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pipx install mcp-attest # isolated CLI
|
|
126
|
+
uvx mcp-attest check --help # one-shot
|
|
127
|
+
pip install mcp-attest # into a virtualenv
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Python 3.10+, zero runtime dependencies.
|
|
131
|
+
|
|
132
|
+
## Use it in CI
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
- uses: Balanov/mcp-attest@v0
|
|
136
|
+
with:
|
|
137
|
+
policy: .attest/policy.json
|
|
138
|
+
trace: .attest/trace.jsonl
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The action fails the job on any critical or high finding, so "our agent policy
|
|
142
|
+
is enforced" becomes something the pipeline proves instead of something the
|
|
143
|
+
security questionnaire asks about.
|
|
144
|
+
|
|
145
|
+
## Policy format
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"version": 1,
|
|
150
|
+
"default_decision": "deny",
|
|
151
|
+
"clients": {
|
|
152
|
+
"ci-bot": {
|
|
153
|
+
"tools": {
|
|
154
|
+
"allow": ["fs__read_file", "fs__list_directory"],
|
|
155
|
+
"deny": ["fs__write_file", "fs__delete_everything"],
|
|
156
|
+
"conditional": [
|
|
157
|
+
{
|
|
158
|
+
"tool": "fs__write_file",
|
|
159
|
+
"allow_when": { "path": { "starts_with": "./out/" } },
|
|
160
|
+
"action": "allow",
|
|
161
|
+
"reason": "writes are allowed only inside ./out/"
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
"egress_hosts": ["api.internal.example.com"]
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"gateway": { "mode": "routed", "fail_closed": true, "passthrough_credentials": false },
|
|
169
|
+
"receipts": { "required": true, "key_env": "MCP_ATTEST_KEY" }
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Condition operators: `eq`, `not_eq`, `starts_with`, `ends_with`, `contains`,
|
|
174
|
+
`in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `glob`, `regex`. A bare string ending
|
|
175
|
+
in `/` is a prefix; a bare string containing `*` is a glob.
|
|
176
|
+
|
|
177
|
+
## Trace format
|
|
178
|
+
|
|
179
|
+
Newline-delimited JSON, one event per line. Any proxy, agent harness or CI
|
|
180
|
+
step can produce it.
|
|
181
|
+
|
|
182
|
+
```jsonl
|
|
183
|
+
{"type":"tools_list","actor":"ci-bot","tools":[{"name":"fs__read_file","description_sha256":"a1b2…"}]}
|
|
184
|
+
{"type":"tool_call","actor":"ci-bot","tool":"fs__read_file","args":{"path":"/srv/README.md"},"decision":"allow","upstream":true,"reason":"tool is on the allow list"}
|
|
185
|
+
{"type":"client_servers","client":"claude-code","servers":["fs","git"]}
|
|
186
|
+
{"type":"gateway_route","server":"fs","tools":["read_file","list_directory"]}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Full specification: [SPEC.md](SPEC.md).
|
|
190
|
+
|
|
191
|
+
## Receipts
|
|
192
|
+
|
|
193
|
+
`attest receipts --trace trace.jsonl --out receipts.jsonl` produces a
|
|
194
|
+
hash-chained receipt per allowed call. Each receipt commits to the previous
|
|
195
|
+
one, so removing or reordering a receipt breaks the chain. With
|
|
196
|
+
`MCP_ATTEST_KEY` set, every receipt is additionally signed with HMAC-SHA256,
|
|
197
|
+
so an auditor can detect edits without access to the gateway.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
export MCP_ATTEST_KEY=…
|
|
201
|
+
attest receipts --trace trace.jsonl --out receipts.jsonl
|
|
202
|
+
# chain: ok (12 receipts, key=yes)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This is deliberately not a PKI. It answers the question an incident responder
|
|
206
|
+
actually asks: *did this session really contain these calls, in this order, and
|
|
207
|
+
has anything been altered since?*
|
|
208
|
+
|
|
209
|
+
## Commands
|
|
210
|
+
|
|
211
|
+
| Command | Purpose |
|
|
212
|
+
|---------|---------|
|
|
213
|
+
| `attest init` | Write a starter policy |
|
|
214
|
+
| `attest check` | Check a trace against a policy (`--json`, `--strict`) |
|
|
215
|
+
| `attest receipts` | Build and verify a hash-chained receipt trail |
|
|
216
|
+
| `attest shadow` | List paths that bypass the control point |
|
|
217
|
+
| `attest rules` | List the enforced rules |
|
|
218
|
+
|
|
219
|
+
## Design principles
|
|
220
|
+
|
|
221
|
+
- **Read-only.** It never sits in the request path and never holds a credential
|
|
222
|
+
for the upstream. It reads evidence.
|
|
223
|
+
- **Evidence over opinion.** Every finding carries the exact line, tool,
|
|
224
|
+
arguments and decision that produced it.
|
|
225
|
+
- **Offline.** No network calls, no telemetry, no account. Traces contain
|
|
226
|
+
payloads and stay where they are.
|
|
227
|
+
- **Exit codes, not vibes.** `0` clean, `1` blocking finding, `2` bad input.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
python -m venv .venv && . .venv/Scripts/activate
|
|
233
|
+
pip install -e ".[dev]"
|
|
234
|
+
python -m unittest discover -s tests -v
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# mcp-attest
|
|
2
|
+
|
|
3
|
+
**Prove that your MCP policy actually enforces.**
|
|
4
|
+
|
|
5
|
+
`mcp-attest` is a conformance and attestation harness for agent access control.
|
|
6
|
+
It takes the policy you *wrote* and the trace of what *actually happened*, and
|
|
7
|
+
reports every point where they disagree — including the failure modes that
|
|
8
|
+
gateways accept silently: a rule that was registered but never enforced, a
|
|
9
|
+
tool that is hidden from `tools/list` but still callable, a client that reaches
|
|
10
|
+
a server outside the gateway, and a tool definition that changed after it was
|
|
11
|
+
approved.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pipx install mcp-attest
|
|
15
|
+
attest init
|
|
16
|
+
attest check --policy policy.json --trace trace.jsonl
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
!! [CRITICAL] C6 Policy denies 'shell__exec' but the gateway allowed it
|
|
21
|
+
Local evaluation of the policy says deny (tool is on the deny list); the
|
|
22
|
+
trace records allow ('argument-conditioned rule matched'). This is the
|
|
23
|
+
'policy accepted but not enforced' failure mode — a rule that is
|
|
24
|
+
registered but never applied.
|
|
25
|
+
fix: Replay the call against the gateway with the same arguments and fix
|
|
26
|
+
the rule binding, then add this call as a regression case in CI.
|
|
27
|
+
|
|
28
|
+
!! [CRITICAL] C9 Tool 'fs__read_file' changed after it was listed
|
|
29
|
+
The description hash of an already-approved tool changed between two
|
|
30
|
+
listings. This is the shape of a rug pull: the tool was harmless at
|
|
31
|
+
approval time.
|
|
32
|
+
|
|
33
|
+
summary: critical=7, high=6
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Exit code is `1` when a blocking finding exists, so it works as a CI gate
|
|
37
|
+
without a wrapper script. The session above is `examples/session.jsonl` — run
|
|
38
|
+
`attest check --policy examples/policy.json --trace examples/session.jsonl` to
|
|
39
|
+
reproduce it.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Why this exists
|
|
44
|
+
|
|
45
|
+
Gateways, policy engines and allowlists for Model Context Protocol are now
|
|
46
|
+
everywhere — Docker MCP Gateway, agentgateway, Bifrost, Runlayer, Lasso, Obot,
|
|
47
|
+
and GitHub's own `gh-aw-mcpg`. The policy language is not the problem.
|
|
48
|
+
|
|
49
|
+
The problem is that **nobody verifies the policy is enforced.** In a detailed
|
|
50
|
+
evaluation of agentgateway published in October 2026, an argument-conditioned
|
|
51
|
+
policy was "reported as accepted and still did not enforce what it said", and
|
|
52
|
+
the conclusion was blunt: "turning a policy on is not the end of the job — send
|
|
53
|
+
real calls and check."
|
|
54
|
+
|
|
55
|
+
The same evaluation found policies that were accepted and had no effect at
|
|
56
|
+
all. The State of MCP Security 2026 survey describes the structural gap:
|
|
57
|
+
"Gateways only protect the servers they route. An agent calling an MCP server
|
|
58
|
+
outside the gateway is invisible," and "Most gateways also do not do deep
|
|
59
|
+
content inspection of responses. The gateway enforces who and where, not what."
|
|
60
|
+
|
|
61
|
+
Meanwhile the threat volume is not theoretical: more than 40 CVEs against MCP
|
|
62
|
+
implementations were disclosed in the first four months of 2026, an estimated
|
|
63
|
+
200,000 servers are exposed, 38% of scanned MCP servers had no authentication
|
|
64
|
+
on critical endpoints, and in April 2026 researchers hijacked Claude Code, Gemini
|
|
65
|
+
CLI and GitHub Copilot through malicious pull-request titles — with bug bounties
|
|
66
|
+
paid by the vendors.
|
|
67
|
+
|
|
68
|
+
And the buyers of this category are being told the same thing. The US DoD's
|
|
69
|
+
Center for Security and Information Technology wrote that "without these
|
|
70
|
+
traceable audit logs, incident response and accountability become significantly
|
|
71
|
+
more difficult, especially in multi-tenant environments."
|
|
72
|
+
|
|
73
|
+
So the missing primitive is small, sharp and useful: **a harness that takes a
|
|
74
|
+
policy and a trace and tells you, with evidence, whether your control plane
|
|
75
|
+
does what you wrote down.**
|
|
76
|
+
|
|
77
|
+
`mcp-attest` is that harness. It is not a gateway, it does not sit on the hot
|
|
78
|
+
path, and it does not need to be trusted with your traffic — it reads what
|
|
79
|
+
already happened.
|
|
80
|
+
|
|
81
|
+
## What it checks
|
|
82
|
+
|
|
83
|
+
| ID | Rule | The failure it catches |
|
|
84
|
+
|----|------|------------------------|
|
|
85
|
+
| C1 | default-deny | An unset allowlist silently meaning "allow everything" |
|
|
86
|
+
| C2 | list/call parity | A tool hidden from `tools/list` but callable by name |
|
|
87
|
+
| C3 | narrowing only | A per-request control widening a client's grant |
|
|
88
|
+
| C4 | credential separation | The client's token replayed upstream verbatim |
|
|
89
|
+
| C5 | fail closed | A call allowed because the policy check errored |
|
|
90
|
+
| C6 | enforcement parity | The policy says deny, the gateway allowed it |
|
|
91
|
+
| C7 | shadow MCP | A server the agent can reach outside the gateway |
|
|
92
|
+
| C8 | receipt chain | Allowed calls with no verifiable receipt, or a broken chain |
|
|
93
|
+
| C9 | description integrity | A tool definition changed after approval (rug pull) |
|
|
94
|
+
|
|
95
|
+
C6 is the one that matters most, and it is the one no existing tool does: it
|
|
96
|
+
re-evaluates your policy locally against every recorded call and flags any call
|
|
97
|
+
whose observed decision disagrees with the policy. That catches the
|
|
98
|
+
"accepted but not enforced" class directly.
|
|
99
|
+
|
|
100
|
+
## Install
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pipx install mcp-attest # isolated CLI
|
|
104
|
+
uvx mcp-attest check --help # one-shot
|
|
105
|
+
pip install mcp-attest # into a virtualenv
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Python 3.10+, zero runtime dependencies.
|
|
109
|
+
|
|
110
|
+
## Use it in CI
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
- uses: Balanov/mcp-attest@v0
|
|
114
|
+
with:
|
|
115
|
+
policy: .attest/policy.json
|
|
116
|
+
trace: .attest/trace.jsonl
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The action fails the job on any critical or high finding, so "our agent policy
|
|
120
|
+
is enforced" becomes something the pipeline proves instead of something the
|
|
121
|
+
security questionnaire asks about.
|
|
122
|
+
|
|
123
|
+
## Policy format
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"version": 1,
|
|
128
|
+
"default_decision": "deny",
|
|
129
|
+
"clients": {
|
|
130
|
+
"ci-bot": {
|
|
131
|
+
"tools": {
|
|
132
|
+
"allow": ["fs__read_file", "fs__list_directory"],
|
|
133
|
+
"deny": ["fs__write_file", "fs__delete_everything"],
|
|
134
|
+
"conditional": [
|
|
135
|
+
{
|
|
136
|
+
"tool": "fs__write_file",
|
|
137
|
+
"allow_when": { "path": { "starts_with": "./out/" } },
|
|
138
|
+
"action": "allow",
|
|
139
|
+
"reason": "writes are allowed only inside ./out/"
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"egress_hosts": ["api.internal.example.com"]
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"gateway": { "mode": "routed", "fail_closed": true, "passthrough_credentials": false },
|
|
147
|
+
"receipts": { "required": true, "key_env": "MCP_ATTEST_KEY" }
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Condition operators: `eq`, `not_eq`, `starts_with`, `ends_with`, `contains`,
|
|
152
|
+
`in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `glob`, `regex`. A bare string ending
|
|
153
|
+
in `/` is a prefix; a bare string containing `*` is a glob.
|
|
154
|
+
|
|
155
|
+
## Trace format
|
|
156
|
+
|
|
157
|
+
Newline-delimited JSON, one event per line. Any proxy, agent harness or CI
|
|
158
|
+
step can produce it.
|
|
159
|
+
|
|
160
|
+
```jsonl
|
|
161
|
+
{"type":"tools_list","actor":"ci-bot","tools":[{"name":"fs__read_file","description_sha256":"a1b2…"}]}
|
|
162
|
+
{"type":"tool_call","actor":"ci-bot","tool":"fs__read_file","args":{"path":"/srv/README.md"},"decision":"allow","upstream":true,"reason":"tool is on the allow list"}
|
|
163
|
+
{"type":"client_servers","client":"claude-code","servers":["fs","git"]}
|
|
164
|
+
{"type":"gateway_route","server":"fs","tools":["read_file","list_directory"]}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Full specification: [SPEC.md](SPEC.md).
|
|
168
|
+
|
|
169
|
+
## Receipts
|
|
170
|
+
|
|
171
|
+
`attest receipts --trace trace.jsonl --out receipts.jsonl` produces a
|
|
172
|
+
hash-chained receipt per allowed call. Each receipt commits to the previous
|
|
173
|
+
one, so removing or reordering a receipt breaks the chain. With
|
|
174
|
+
`MCP_ATTEST_KEY` set, every receipt is additionally signed with HMAC-SHA256,
|
|
175
|
+
so an auditor can detect edits without access to the gateway.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
export MCP_ATTEST_KEY=…
|
|
179
|
+
attest receipts --trace trace.jsonl --out receipts.jsonl
|
|
180
|
+
# chain: ok (12 receipts, key=yes)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
This is deliberately not a PKI. It answers the question an incident responder
|
|
184
|
+
actually asks: *did this session really contain these calls, in this order, and
|
|
185
|
+
has anything been altered since?*
|
|
186
|
+
|
|
187
|
+
## Commands
|
|
188
|
+
|
|
189
|
+
| Command | Purpose |
|
|
190
|
+
|---------|---------|
|
|
191
|
+
| `attest init` | Write a starter policy |
|
|
192
|
+
| `attest check` | Check a trace against a policy (`--json`, `--strict`) |
|
|
193
|
+
| `attest receipts` | Build and verify a hash-chained receipt trail |
|
|
194
|
+
| `attest shadow` | List paths that bypass the control point |
|
|
195
|
+
| `attest rules` | List the enforced rules |
|
|
196
|
+
|
|
197
|
+
## Design principles
|
|
198
|
+
|
|
199
|
+
- **Read-only.** It never sits in the request path and never holds a credential
|
|
200
|
+
for the upstream. It reads evidence.
|
|
201
|
+
- **Evidence over opinion.** Every finding carries the exact line, tool,
|
|
202
|
+
arguments and decision that produced it.
|
|
203
|
+
- **Offline.** No network calls, no telemetry, no account. Traces contain
|
|
204
|
+
payloads and stay where they are.
|
|
205
|
+
- **Exit codes, not vibes.** `0` clean, `1` blocking finding, `2` bad input.
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
python -m venv .venv && . .venv/Scripts/activate
|
|
211
|
+
pip install -e ".[dev]"
|
|
212
|
+
python -m unittest discover -s tests -v
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcp-attest"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Conformance and attestation harness for MCP agent access control: prove that a policy actually enforces, detect shadow MCP paths, and emit tamper-evident receipts."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Balanov.PRO" }]
|
|
13
|
+
keywords = ["mcp", "model-context-protocol", "security", "policy", "audit", "attestation", "agents"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: Information Technology",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Security",
|
|
21
|
+
"Topic :: Software Development :: Testing",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/Balanov/mcp-attest"
|
|
27
|
+
Source = "https://github.com/Balanov/mcp-attest"
|
|
28
|
+
Issues = "https://github.com/Balanov/mcp-attest/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
attest = "mcp_attest.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""mcp-attest — conformance and attestation harness for MCP agent access control."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .checks import RULES, Finding, run_checks, summarize
|
|
6
|
+
from .policy import Policy, load_policy, policy_from_dict
|
|
7
|
+
from .receipts import build_receipts, verify_receipts
|
|
8
|
+
from .shadow import find_shadow_paths
|
|
9
|
+
from .trace import Trace, load_trace
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"RULES",
|
|
13
|
+
"Finding",
|
|
14
|
+
"Policy",
|
|
15
|
+
"Trace",
|
|
16
|
+
"build_receipts",
|
|
17
|
+
"find_shadow_paths",
|
|
18
|
+
"load_policy",
|
|
19
|
+
"load_trace",
|
|
20
|
+
"policy_from_dict",
|
|
21
|
+
"run_checks",
|
|
22
|
+
"summarize",
|
|
23
|
+
"verify_receipts",
|
|
24
|
+
]
|