trustdex 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/bin/trustdex.mjs +21 -7
- package/docs/CLAUDE.md +133 -0
- package/docs/CODEX.md +4 -4
- package/docs/EVALUATION.md +35 -0
- package/docs/RELEASING.md +18 -1
- package/docs/SECURITY_RESEARCH.md +60 -0
- package/package.json +3 -2
- package/src/claude-config.mjs +106 -0
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ TrustDex deliberately does not infer "official" status from names, stars, or bra
|
|
|
54
54
|
- run as a GitHub Action in CI
|
|
55
55
|
- fingerprint MCP entries, skills, and plugin manifests without storing secret values
|
|
56
56
|
- inspect and filter Codex `config.toml` MCP sections
|
|
57
|
+
- inspect and filter Claude Code MCP JSON with explicit transport validation and header execution controls
|
|
57
58
|
- perform explicit provenance lookups against GitHub, npm, and the official MCP Registry
|
|
58
59
|
- verify whether the latest GitHub release tag/commit has a GitHub-verified signature
|
|
59
60
|
- create signed review bundles and automatically require re-review when trust-relevant state drifts
|
|
@@ -75,8 +76,8 @@ Requires Node.js 20+.
|
|
|
75
76
|
Run directly from npm:
|
|
76
77
|
|
|
77
78
|
```bash
|
|
78
|
-
npx trustdex@0.
|
|
79
|
-
npx trustdex@0.
|
|
79
|
+
npx trustdex@0.5.0 --help
|
|
80
|
+
npx trustdex@0.5.0 inspect ./mcp.json --pack strict --policy ./trustdex.policy.json
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
Or work from a source checkout:
|
|
@@ -100,6 +101,23 @@ Exit codes for inspection:
|
|
|
100
101
|
- `1` - at least one entry needs review
|
|
101
102
|
- `2` - at least one entry is blocked, or the command failed
|
|
102
103
|
|
|
104
|
+
## Reproducible defensive research
|
|
105
|
+
|
|
106
|
+
TrustDex includes an inert adversarial benchmark for testing the policy boundary without contacting or executing third-party services. The fixtures use reserved `.invalid` domains, placeholder credentials, and configuration data only; TrustDex inspects them statically and never launches the listed servers.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm run test:research
|
|
110
|
+
|
|
111
|
+
node ./bin/trustdex.mjs inspect \
|
|
112
|
+
./examples/adversarial/mcp-config.json \
|
|
113
|
+
--pack strict \
|
|
114
|
+
--policy ./examples/adversarial/policy.json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The direct inspection intentionally exits with code `2`: the checked-in expectation is one `ALLOW`, one `ASK`, and five `BLOCK` decisions. The automated test also verifies that placeholder secret values do not appear in results and that a newly introduced secret-bearing environment variable invalidates the previous review state.
|
|
118
|
+
|
|
119
|
+
See the [defensive security research policy](docs/SECURITY_RESEARCH.md), [evaluation record](docs/EVALUATION.md), and [adversarial fixture guide](examples/adversarial/README.md).
|
|
120
|
+
|
|
103
121
|
## Built-in policy packs
|
|
104
122
|
|
|
105
123
|
```bash
|
|
@@ -149,6 +167,26 @@ The gate removes non-approved MCP sections while preserving unrelated TOML secti
|
|
|
149
167
|
|
|
150
168
|
See [Using TrustDex with Codex](docs/CODEX.md) for a step-by-step gating workflow and for how this project uses Codex in its own review process.
|
|
151
169
|
|
|
170
|
+
## Claude Code MCP configuration
|
|
171
|
+
|
|
172
|
+
Claude Code uses the `mcpServers` JSON shape. Use the Claude-specific commands so
|
|
173
|
+
transport changes, authentication header metadata, and header helper commands
|
|
174
|
+
are included in inspection and signed review:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
node ./bin/trustdex.mjs inspect-claude ./.mcp.json --pack strict
|
|
178
|
+
node ./bin/trustdex.mjs gate-claude ./.mcp.json --pack strict --out .trustdex/claude-mcp.json
|
|
179
|
+
claude --strict-mcp-config --mcp-config .trustdex/claude-mcp.json
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Check the gate's result before starting Claude. Only `ALLOW` entries are exposed by
|
|
183
|
+
default; `ASK` entries require review. The adapter rejects settings it cannot
|
|
184
|
+
inspect instead of silently trusting them. These commands are available in npm 0.5.0 and later, and from a source checkout.
|
|
185
|
+
Use `npx trustdex@0.5.0` in place of `node ./bin/trustdex.mjs` to run the published CLI.
|
|
186
|
+
|
|
187
|
+
See [Using TrustDex with Claude](docs/CLAUDE.md) for supported settings, PowerShell
|
|
188
|
+
error handling, signed review, and the Claude Desktop workflow.
|
|
189
|
+
|
|
152
190
|
## Online provenance observations
|
|
153
191
|
|
|
154
192
|
Normal inspection remains local-only. These commands perform an explicit network lookup only when invoked:
|
|
@@ -294,7 +332,7 @@ The private key should never be committed.
|
|
|
294
332
|
```yaml
|
|
295
333
|
- uses: actions/checkout@v4
|
|
296
334
|
|
|
297
|
-
- uses: grigent/trustdex@v0.
|
|
335
|
+
- uses: grigent/trustdex@v0.5.0
|
|
298
336
|
with:
|
|
299
337
|
config: ./mcp.json
|
|
300
338
|
pack: official-first
|
|
@@ -322,7 +360,7 @@ The public roadmap is maintained in [docs/ROADMAP.md](docs/ROADMAP.md). Near-ter
|
|
|
322
360
|
|
|
323
361
|
TrustDex does **not** prove that third-party code is safe and does not replace sandboxing, dependency scanning, code review, or least-privilege credentials.
|
|
324
362
|
|
|
325
|
-
See [SECURITY.md](SECURITY.md), [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md), and [docs/TRUST_STORE.md](docs/TRUST_STORE.md).
|
|
363
|
+
See [SECURITY.md](SECURITY.md), [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md), [docs/SECURITY_RESEARCH.md](docs/SECURITY_RESEARCH.md), [docs/EVALUATION.md](docs/EVALUATION.md), and [docs/TRUST_STORE.md](docs/TRUST_STORE.md).
|
|
326
364
|
|
|
327
365
|
## Contributing
|
|
328
366
|
|
package/bin/trustdex.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import process from 'node:process';
|
|
|
6
6
|
|
|
7
7
|
import { inspectMcpConfig } from '../src/inspect.mjs';
|
|
8
8
|
import { parseCodexMcpToml, gateCodexToml } from '../src/codex-config.mjs';
|
|
9
|
+
import { parseClaudeMcpConfig } from '../src/claude-config.mjs';
|
|
9
10
|
import { inspectSkillText, inspectPluginManifest } from '../src/extensions.mjs';
|
|
10
11
|
import { evaluateAll } from '../src/policy.mjs';
|
|
11
12
|
import { getPolicyPack, mergePolicy } from '../src/policy-packs.mjs';
|
|
@@ -48,6 +49,13 @@ Usage:
|
|
|
48
49
|
trustdex gate-codex <config.toml> --out gated.toml [--pack ...] [--policy policy.json]
|
|
49
50
|
[--trust-store trust-store.json] [--include-ask]
|
|
50
51
|
|
|
52
|
+
trustdex inspect-claude <.mcp.json> [--pack ...] [--policy policy.json] [--json]
|
|
53
|
+
trustdex gate-claude <.mcp.json> --out gated.json [--pack ...] [--policy policy.json]
|
|
54
|
+
trustdex snapshot-claude <.mcp.json> --out snapshot.json [--pack ...]
|
|
55
|
+
trustdex approve-claude <.mcp.json> --private key.pem --dir review [--pack ...]
|
|
56
|
+
trustdex recheck-claude <.mcp.json> --review-dir review --public key.pem [--json]
|
|
57
|
+
Claude commands accept the same policy/trust-store flags as their generic counterparts.
|
|
58
|
+
|
|
51
59
|
trustdex provenance github <owner/repo> [--json]
|
|
52
60
|
trustdex provenance github-release <owner/repo> [--json]
|
|
53
61
|
trustdex provenance npm <package> [--json]
|
|
@@ -146,8 +154,14 @@ function setInspectExitCode(results) {
|
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
async function evaluateMcp(configPath, args) {
|
|
149
|
-
const
|
|
150
|
-
|
|
157
|
+
const isClaude = args[0].endsWith('-claude');
|
|
158
|
+
let config;
|
|
159
|
+
try { config = await readJson(configPath); } catch (error) {
|
|
160
|
+
if (isClaude && error instanceof SyntaxError) throw new Error('Invalid Claude MCP JSON.');
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
const inspectionConfig = isClaude ? parseClaudeMcpConfig(config) : config;
|
|
164
|
+
const inspected = inspectMcpConfig(inspectionConfig);
|
|
151
165
|
const store = await loadTrustStore(args);
|
|
152
166
|
const withProvenance = store ? attachProvenanceAll(inspected, store) : inspected;
|
|
153
167
|
const policy = await loadPolicy(args);
|
|
@@ -225,7 +239,7 @@ async function main() {
|
|
|
225
239
|
return;
|
|
226
240
|
}
|
|
227
241
|
|
|
228
|
-
if (command === 'inspect') {
|
|
242
|
+
if (command === 'inspect' || command === 'inspect-claude') {
|
|
229
243
|
const configPath = args[1];
|
|
230
244
|
if (!configPath) throw new Error('inspect requires a path to an MCP JSON file.');
|
|
231
245
|
|
|
@@ -311,7 +325,7 @@ async function main() {
|
|
|
311
325
|
return;
|
|
312
326
|
}
|
|
313
327
|
|
|
314
|
-
if (command === 'gate') {
|
|
328
|
+
if (command === 'gate' || command === 'gate-claude') {
|
|
315
329
|
const configPath = args[1];
|
|
316
330
|
const out = getFlag(args, '--out');
|
|
317
331
|
if (!configPath || !out) throw new Error('gate requires <mcp.json> and --out <gated.json>.');
|
|
@@ -332,7 +346,7 @@ async function main() {
|
|
|
332
346
|
return;
|
|
333
347
|
}
|
|
334
348
|
|
|
335
|
-
if (command === 'approve') {
|
|
349
|
+
if (command === 'approve' || command === 'approve-claude') {
|
|
336
350
|
const configPath = args[1];
|
|
337
351
|
const privatePath = getFlag(args, '--private');
|
|
338
352
|
const reviewDir = getFlag(args, '--dir');
|
|
@@ -364,7 +378,7 @@ async function main() {
|
|
|
364
378
|
return;
|
|
365
379
|
}
|
|
366
380
|
|
|
367
|
-
if (command === 'recheck') {
|
|
381
|
+
if (command === 'recheck' || command === 'recheck-claude') {
|
|
368
382
|
const configPath = args[1];
|
|
369
383
|
const reviewDir = getFlag(args, '--review-dir');
|
|
370
384
|
const publicPath = getFlag(args, '--public');
|
|
@@ -409,7 +423,7 @@ async function main() {
|
|
|
409
423
|
return;
|
|
410
424
|
}
|
|
411
425
|
|
|
412
|
-
if (command === 'snapshot') {
|
|
426
|
+
if (command === 'snapshot' || command === 'snapshot-claude') {
|
|
413
427
|
const configPath = args[1];
|
|
414
428
|
const out = getFlag(args, '--out');
|
|
415
429
|
if (!configPath || !out) throw new Error('snapshot requires <mcp.json> and --out <snapshot.json>.');
|
package/docs/CLAUDE.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Using TrustDex with Claude
|
|
2
|
+
|
|
3
|
+
TrustDex can inspect Claude MCP JSON before Claude Code or Claude Desktop receives it.
|
|
4
|
+
It evaluates configuration evidence locally, does not start MCP servers, and does not
|
|
5
|
+
upload your configuration. An ALLOW decision means the evidence matched your policy;
|
|
6
|
+
it does not prove that server code is safe.
|
|
7
|
+
|
|
8
|
+
The Claude-specific commands are available in TrustDex 0.5.0 and later.
|
|
9
|
+
Use `npx trustdex@0.5.0` in place of `node ./bin/trustdex.mjs` in the examples
|
|
10
|
+
below to run the published CLI, or use the commands directly from a source checkout.
|
|
11
|
+
|
|
12
|
+
## Supported configuration
|
|
13
|
+
|
|
14
|
+
Use a dedicated JSON file with a top-level `mcpServers` object:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"reviewed-tools": {
|
|
20
|
+
"type": "http",
|
|
21
|
+
"url": "https://your-reviewed-server.example/mcp"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The URL above is illustrative; configure your own reviewed server and trust policy.
|
|
28
|
+
|
|
29
|
+
Supported server fields:
|
|
30
|
+
|
|
31
|
+
| Transport | Fields |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `stdio` (also the default when `type` is omitted) | `type`, `command`, `args`, `env` |
|
|
34
|
+
| `http` or `sse` | `type`, `url`, `headers`, `headersHelper` |
|
|
35
|
+
|
|
36
|
+
Remote entries require an explicit `type`. Command arguments and environment/header
|
|
37
|
+
values must be strings. Unknown root/server fields, other transports, malformed
|
|
38
|
+
entries, unresolved command/argument/endpoint variables, and URLs containing
|
|
39
|
+
credentials, query parameters, or fragments fail closed. This intentionally excludes
|
|
40
|
+
custom `oauth`, `alwaysLoad`, WebSocket settings, and future controls until they have
|
|
41
|
+
an explicit inspection implementation. Ordinary server-managed OAuth needs no
|
|
42
|
+
`oauth` field and remains Claude's responsibility.
|
|
43
|
+
|
|
44
|
+
Authentication headers add `static-http-headers` and/or `env-http-headers` signals.
|
|
45
|
+
Built-in packs require review for them. `headersHelper` adds
|
|
46
|
+
`header-helper-command`, which built-in packs block because Claude executes it in a
|
|
47
|
+
shell. TrustDex never runs the helper.
|
|
48
|
+
|
|
49
|
+
Fingerprints include the transport, header names, referenced environment-variable
|
|
50
|
+
names, and helper presence. Header/variable values, defaults, and helper command text
|
|
51
|
+
are omitted. Rotating a credential value alone does not invalidate a review; changing
|
|
52
|
+
its header name or environment reference does. Avoid putting credentials in command
|
|
53
|
+
arguments or URL paths. Gated files preserve original allowed entries and can contain
|
|
54
|
+
credentials supplied in the input: keep them local and out of version control.
|
|
55
|
+
|
|
56
|
+
## Claude Code
|
|
57
|
+
|
|
58
|
+
Claude Code reads project MCP configuration from `.mcp.json`. Inspect it using the
|
|
59
|
+
Claude adapter, then supply only the generated configuration for that session.
|
|
60
|
+
|
|
61
|
+
From a source checkout in PowerShell:
|
|
62
|
+
|
|
63
|
+
```powershell
|
|
64
|
+
node ./bin/trustdex.mjs inspect-claude ./.mcp.json --pack strict --policy ./trustdex.policy.json
|
|
65
|
+
|
|
66
|
+
node ./bin/trustdex.mjs gate-claude ./.mcp.json --pack strict --policy ./trustdex.policy.json --out ./.trustdex/claude-mcp.json
|
|
67
|
+
if ($LASTEXITCODE -ne 0) { throw "Resolve the gate error or pending reviews before starting Claude." }
|
|
68
|
+
|
|
69
|
+
claude --strict-mcp-config --mcp-config ./.trustdex/claude-mcp.json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The policy file is your explicit trust policy, not a list inferred from vendor names.
|
|
73
|
+
Omit `--policy` to use the strict defaults, which block unknown sources and ask about
|
|
74
|
+
local programs. By default, the gate includes only ALLOW entries. ASK and BLOCK entries
|
|
75
|
+
stay out. A gate exit code of `1` indicates pending ASK decisions; `2` indicates failure.
|
|
76
|
+
Do not launch using an old output file after a failed gate.
|
|
77
|
+
|
|
78
|
+
The `--strict-mcp-config` flag matters: `--mcp-config` alone adds a configuration
|
|
79
|
+
alongside other MCP sources. Organizations using managed MCP configuration should
|
|
80
|
+
also verify the managed policy's effective servers. This workflow does not gate
|
|
81
|
+
Claude's built-in tools or inspect every skill/plugin automatically.
|
|
82
|
+
|
|
83
|
+
Inside the resulting Claude Code session, use `/mcp` to verify the actual loaded
|
|
84
|
+
servers. Configuration/unit tests do not replace that runtime check.
|
|
85
|
+
|
|
86
|
+
Official references:
|
|
87
|
+
[Claude Code MCP](https://code.claude.com/docs/en/mcp) and
|
|
88
|
+
[CLI flags](https://code.claude.com/docs/en/cli-reference).
|
|
89
|
+
|
|
90
|
+
## Signed review and drift
|
|
91
|
+
|
|
92
|
+
Keep the Claude adapter throughout a review workflow; generic MCP commands do not
|
|
93
|
+
inspect Claude-specific controls.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
node ./bin/trustdex.mjs keygen --private .trustdex/private.pem --public .trustdex/public.pem
|
|
97
|
+
node ./bin/trustdex.mjs approve-claude ./.mcp.json --private .trustdex/private.pem --dir .trustdex/claude-review --pack strict --policy ./trustdex.policy.json
|
|
98
|
+
node ./bin/trustdex.mjs recheck-claude ./.mcp.json --review-dir .trustdex/claude-review --public .trustdex/public.pem
|
|
99
|
+
node ./bin/trustdex.mjs snapshot-claude ./.mcp.json --out .trustdex/claude-snapshot.json --pack strict --policy ./trustdex.policy.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Approval refuses BLOCK decisions. ASK decisions require an explicit human review;
|
|
103
|
+
running `approve-claude` records that review but does not automatically change the
|
|
104
|
+
gate's policy. Rechecks return APPROVED, NEEDS_REVIEW, or INVALID. A pre-existing
|
|
105
|
+
generic review has a different fingerprint and must be reviewed with the Claude adapter.
|
|
106
|
+
Private keys must never be committed.
|
|
107
|
+
|
|
108
|
+
## Claude Desktop
|
|
109
|
+
|
|
110
|
+
For a local stdio MCP configuration, export just the `mcpServers` object from
|
|
111
|
+
`claude_desktop_config.json` into a dedicated input file. Do not feed unrelated
|
|
112
|
+
Desktop preferences into the MCP adapter. Use `inspect-claude` and `gate-claude`
|
|
113
|
+
on that file, review the result, then replace only the Desktop config's `mcpServers`
|
|
114
|
+
section with the gated section and restart Claude Desktop. Keep a backup of the
|
|
115
|
+
original configuration.
|
|
116
|
+
|
|
117
|
+
TrustDex does not modify your active Desktop settings or install a connector.
|
|
118
|
+
Desktop extensions and account-level remote connectors require separate review;
|
|
119
|
+
this JSON gate does not automatically govern them.
|
|
120
|
+
|
|
121
|
+
## Inert example
|
|
122
|
+
|
|
123
|
+
`examples/claude.mcp.json` uses reserved `.invalid` domains, a placeholder
|
|
124
|
+
environment-variable reference, and an inert helper string. Do not launch it in
|
|
125
|
+
Claude. It is for static inspection only:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
node ./bin/trustdex.mjs inspect-claude ./examples/claude.mcp.json --pack strict --policy ./examples/claude.policy.json
|
|
129
|
+
node ./bin/trustdex.mjs gate-claude ./examples/claude.mcp.json --pack strict --policy ./examples/claude.policy.json --out .trustdex/example-claude.json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Inspection intentionally exits `2`. The expected decisions are one ALLOW, one ASK,
|
|
133
|
+
and two BLOCK; the gate writes only `reviewed` and exits `1` because a review remains.
|
package/docs/CODEX.md
CHANGED
|
@@ -11,7 +11,7 @@ Codex clients read MCP server definitions from `[mcp_servers.<name>]` tables in
|
|
|
11
11
|
### 1. Inspect the current configuration
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx trustdex@0.4.
|
|
14
|
+
npx trustdex@0.4.4 inspect-codex ~/.codex/config.toml --pack strict
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
This reads the file locally and makes no network request. The output lists each MCP server, its source type, trust signals, and an ALLOW / ASK / BLOCK decision. Environment variable names may appear; their values never do.
|
|
@@ -21,7 +21,7 @@ Use `--json` for machine-readable output and `--trust-store ./trust-store.json`
|
|
|
21
21
|
### 2. Write a gated copy
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npx trustdex@0.4.
|
|
24
|
+
npx trustdex@0.4.4 gate-codex ~/.codex/config.toml \
|
|
25
25
|
--pack official-first \
|
|
26
26
|
--trust-store ./trust-store.json \
|
|
27
27
|
--out ./gated-config.toml
|
|
@@ -52,8 +52,8 @@ TrustDex parses only the TOML constructs used by Codex MCP sections. An unsuppor
|
|
|
52
52
|
Agent skills and plugin manifests can be inspected the same way:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
npx trustdex@0.4.
|
|
56
|
-
npx trustdex@0.4.
|
|
55
|
+
npx trustdex@0.4.4 inspect-skill ./path/to/SKILL.md --pack strict
|
|
56
|
+
npx trustdex@0.4.4 inspect-plugin ./path/to/plugin.json --pack strict
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
These report observable signals such as piped shell installers, references to sensitive paths, network URLs, and wildcard tool scopes. They do not prove a skill or plugin is safe.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Defensive evaluation
|
|
2
|
+
|
|
3
|
+
This evaluation records the behavior enforced by the checked-in adversarial fixtures. Run `npm run test:research` to reproduce it. The test performs local static inspection only and does not execute or contact any configured server.
|
|
4
|
+
|
|
5
|
+
## MCP policy cases
|
|
6
|
+
|
|
7
|
+
The cases use the built-in `strict` policy with the small overlay in `examples/adversarial/policy.json`.
|
|
8
|
+
|
|
9
|
+
| Case | Required evidence | Expected decision | Reason |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| `trusted-pinned-package` | pinned package; explicitly trusted package name | `ALLOW` | the configured version is fixed and the policy contains an explicit package trust rule |
|
|
12
|
+
| `floating-version` | `install-on-run`, `unbounded-version` | `BLOCK` | a moving package version can change without review |
|
|
13
|
+
| `shell-launcher` | `shell-execution`, `filesystem-path` | `BLOCK` | shell execution is blocked by the strict policy |
|
|
14
|
+
| `credential-bearing-url` | `embedded-credentials`, `network-endpoint`, `url-query-parameters` | `BLOCK` | embedded credentials are blocking even when the host is allowlisted |
|
|
15
|
+
| `insecure-remote` | `insecure-transport`, `network-endpoint` | `BLOCK` | plaintext transport is blocking even when the host is allowlisted |
|
|
16
|
+
| `sensitive-local-env` | `filesystem-path`, `secret-env` | `ASK` | a local extension with a secret-like environment name requires review |
|
|
17
|
+
| `unknown-remote` | `network-endpoint` | `BLOCK` | the remote host has no explicit allowlist or provenance evidence |
|
|
18
|
+
|
|
19
|
+
Expected total: one `ALLOW`, one `ASK`, and five `BLOCK` decisions.
|
|
20
|
+
|
|
21
|
+
## Drift case
|
|
22
|
+
|
|
23
|
+
The drift fixture begins with the trusted pinned package and then introduces an `API_TOKEN` environment key. The regression test requires all of the following changes:
|
|
24
|
+
|
|
25
|
+
- `signals-added` for `secret-env`
|
|
26
|
+
- `decision-changed` from `ALLOW` to `ASK`
|
|
27
|
+
- `fingerprint-changed`
|
|
28
|
+
|
|
29
|
+
The placeholder value itself must not appear in either inspection result or snapshot.
|
|
30
|
+
|
|
31
|
+
## What this does not measure
|
|
32
|
+
|
|
33
|
+
This benchmark is intentionally narrow. It does not measure runtime behavior, sandbox escape, arbitrary malware, every form of prompt injection, publisher identity ownership, or vulnerabilities in transitive dependencies. In particular, tool descriptions returned dynamically by an already-running MCP server are outside the current static configuration boundary.
|
|
34
|
+
|
|
35
|
+
The small fixture set is a regression baseline, not a statistical claim about detection accuracy. False-positive and false-negative rates require a larger, independently reviewed corpus of redacted real-world configurations.
|
package/docs/RELEASING.md
CHANGED
|
@@ -30,12 +30,29 @@ Once trusted publishing works, remove the `NPM_TOKEN` repository secret and revo
|
|
|
30
30
|
|
|
31
31
|
Both release workflows run `npm pkg fix` and require a clean `package.json` diff before packing or publishing. This prevents npm from silently normalizing away critical metadata such as the CLI entrypoint.
|
|
32
32
|
|
|
33
|
-
## Provenance
|
|
33
|
+
## Provenance and release evidence
|
|
34
34
|
|
|
35
35
|
The repository is public and the package is public, so GitHub-hosted publishing can emit npm provenance. The workflow also passes `--provenance` explicitly.
|
|
36
36
|
|
|
37
37
|
Provenance links the published artifact to its source/build environment. It does not prove that the package contains no malicious behavior.
|
|
38
38
|
|
|
39
|
+
The GitHub release workflow also creates and uploads:
|
|
40
|
+
|
|
41
|
+
- the npm package tarball built from the release commit
|
|
42
|
+
- a CycloneDX JSON SBOM
|
|
43
|
+
- a `SHA256SUMS` file covering the tarball and SBOM
|
|
44
|
+
- a GitHub build-provenance attestation for the tarball
|
|
45
|
+
- a GitHub SBOM attestation binding the SBOM to the tarball
|
|
46
|
+
|
|
47
|
+
After downloading a release, verify its checksum and GitHub attestation before inspection or installation:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
shasum -a 256 -c SHA256SUMS
|
|
51
|
+
gh attestation verify trustdex-<version>.tgz --repo grigent/trustdex
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The npm provenance statement and GitHub attestations cover different publication paths. Neither is a malware analysis or a substitute for reviewing the release contents and the documented trust boundary.
|
|
55
|
+
|
|
39
56
|
## Version checklist
|
|
40
57
|
|
|
41
58
|
Before each release:
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Defensive security research
|
|
2
|
+
|
|
3
|
+
TrustDex is developed for legitimate defensive research into trust decisions made before AI agents receive access to third-party tools. This document defines the authorization, safety, and disclosure boundaries for that work.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
The research asks whether observable configuration, provenance, integrity, and capability evidence is sufficient to keep an extension unavailable until a human or policy explicitly trusts it. Current work focuses on:
|
|
8
|
+
|
|
9
|
+
- unpinned or substituted packages
|
|
10
|
+
- install-on-run and shell launchers
|
|
11
|
+
- embedded URL credentials and insecure remote transport
|
|
12
|
+
- secret-bearing environment variable names without retaining their values
|
|
13
|
+
- source, provenance, decision, fingerprint, and capability drift
|
|
14
|
+
- signed review records and re-review after trust-relevant change
|
|
15
|
+
- parser behavior that must fail closed on unsupported syntax
|
|
16
|
+
|
|
17
|
+
The intended outputs are reproducible fixtures, regression tests, documented limitations, and practical mitigations for agent developers and operators.
|
|
18
|
+
|
|
19
|
+
## Authorization boundary
|
|
20
|
+
|
|
21
|
+
Research is limited to:
|
|
22
|
+
|
|
23
|
+
- systems and accounts owned by the researcher; or
|
|
24
|
+
- systems for which the researcher has explicit authorization to test; and
|
|
25
|
+
- isolated local or purpose-built test environments using non-sensitive fixtures.
|
|
26
|
+
|
|
27
|
+
TrustDex research does not authorize testing unrelated public systems. It excludes credential theft, persistence, destructive actions, data exfiltration, mass exploitation, unauthorized access, malware deployment, and bypassing another party's safeguards.
|
|
28
|
+
|
|
29
|
+
## Safe test data
|
|
30
|
+
|
|
31
|
+
Checked-in adversarial fixtures are inert configuration documents. They use reserved `.invalid` domains, nonexistent package namespaces, and unmistakable placeholder values. TrustDex does not execute MCP servers during inspection.
|
|
32
|
+
|
|
33
|
+
Tests assert that secret-like values, URL credentials, query values, and private paths do not appear in inspection results. A fixture that requires a real credential, private endpoint, personal data, or production service is not acceptable for this repository.
|
|
34
|
+
|
|
35
|
+
## Method
|
|
36
|
+
|
|
37
|
+
Each research case should contain:
|
|
38
|
+
|
|
39
|
+
1. a minimal, reviewable input fixture
|
|
40
|
+
2. the expected trust-relevant signals
|
|
41
|
+
3. the expected `ALLOW`, `ASK`, or `BLOCK` decision under a named policy
|
|
42
|
+
4. an automated regression test
|
|
43
|
+
5. a documented limitation or plausible bypass condition
|
|
44
|
+
|
|
45
|
+
An `ALLOW` result means only that observed evidence matched the selected policy. It is never represented as proof that arbitrary code is safe.
|
|
46
|
+
|
|
47
|
+
## Handling findings
|
|
48
|
+
|
|
49
|
+
Potential TrustDex vulnerabilities that could put users at risk should follow [SECURITY.md](../SECURITY.md). Findings in another project should be disclosed through that project's authorized security channel and should not be published before a reasonable coordinated-disclosure process.
|
|
50
|
+
|
|
51
|
+
Public reports must remove credentials, private endpoints, proprietary prompts, personal data, and exploit details that would create unnecessary risk. Research notes should prefer the minimum evidence needed to reproduce and fix the issue.
|
|
52
|
+
|
|
53
|
+
## Current reproducible evidence
|
|
54
|
+
|
|
55
|
+
- [Adversarial MCP fixtures](../examples/adversarial/README.md)
|
|
56
|
+
- [Evaluation record](EVALUATION.md)
|
|
57
|
+
- [Threat model](THREAT_MODEL.md)
|
|
58
|
+
- [Security policy](../SECURITY.md)
|
|
59
|
+
|
|
60
|
+
The project welcomes verifiable pilot feedback, including false positives and false negatives, through the repository's Pilot feedback issue form. Popularity, branding, and unverifiable testimonials are not treated as security evidence.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trustdex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A local-first trust, provenance, and review gate for AI agent tools, MCP servers, skills, and plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "node --test",
|
|
11
|
-
"
|
|
11
|
+
"test:research": "node --test test/research-fixtures.test.mjs",
|
|
12
|
+
"check": "node --check bin/trustdex.mjs && node --check src/canonical.mjs && node --check src/claude-config.mjs && node --check src/codex-config.mjs && node --check src/extensions.mjs && node --check src/gate.mjs && node --check src/inspect.mjs && node --check src/policy.mjs && node --check src/policy-packs.mjs && node --check src/provenance.mjs && node --check src/provenance-online.mjs && node --check src/review.mjs && node --check src/snapshot.mjs && node --check src/trust-record.mjs",
|
|
12
13
|
"prepublishOnly": "npm test && npm run check"
|
|
13
14
|
},
|
|
14
15
|
"engines": {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const SERVER_KEYS = {
|
|
2
|
+
stdio: new Set(['type', 'command', 'args', 'env']),
|
|
3
|
+
http: new Set(['type', 'url', 'headers', 'headersHelper']),
|
|
4
|
+
sse: new Set(['type', 'url', 'headers', 'headersHelper'])
|
|
5
|
+
};
|
|
6
|
+
const ENV_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}/g;
|
|
7
|
+
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function requireKeys(value, allowed, context) {
|
|
13
|
+
if (Object.keys(value).some((key) => !allowed.has(key))) {
|
|
14
|
+
throw new Error(`Unsupported ${context} field. Claude MCP inspection fails closed on unknown settings.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function environmentReferences(value) {
|
|
19
|
+
return [...new Set([...value.matchAll(ENV_REFERENCE)].map((match) => match[1]))].sort();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Return inspection input only. Gating must preserve entries from the original
|
|
23
|
+
// configuration, never these internal adapter annotations.
|
|
24
|
+
export function parseClaudeMcpConfig(config) {
|
|
25
|
+
if (!isRecord(config) || !isRecord(config.mcpServers)) {
|
|
26
|
+
throw new Error('Expected a Claude MCP JSON object containing an mcpServers object.');
|
|
27
|
+
}
|
|
28
|
+
requireKeys(config, new Set(['mcpServers']), 'Claude MCP root');
|
|
29
|
+
|
|
30
|
+
const entries = Object.entries(config.mcpServers).map(([name, server]) => {
|
|
31
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name) || ['__proto__', 'constructor', 'prototype'].includes(name)) {
|
|
32
|
+
throw new Error('Unsupported Claude MCP server name.');
|
|
33
|
+
}
|
|
34
|
+
if (!isRecord(server)) throw new Error('Each Claude MCP server must be an object.');
|
|
35
|
+
const type = server.type === undefined ? 'stdio' : server.type;
|
|
36
|
+
if (typeof type !== 'string' || !Object.hasOwn(SERVER_KEYS, type)) {
|
|
37
|
+
throw new Error('Unsupported Claude MCP transport. Supported: stdio, http, sse.');
|
|
38
|
+
}
|
|
39
|
+
requireKeys(server, SERVER_KEYS[type], 'Claude MCP server');
|
|
40
|
+
const signals = [];
|
|
41
|
+
const adapter = { runtime: 'claude-mcp-v1', transport: type };
|
|
42
|
+
|
|
43
|
+
if (type === 'stdio') {
|
|
44
|
+
if (typeof server.command !== 'string' || !server.command.trim()) {
|
|
45
|
+
throw new Error('Claude stdio MCP servers require a non-empty command.');
|
|
46
|
+
}
|
|
47
|
+
if (server.command.includes('${')) {
|
|
48
|
+
throw new Error('Claude command variable expansion is unsupported; use an explicit executable.');
|
|
49
|
+
}
|
|
50
|
+
if (server.args !== undefined && (!Array.isArray(server.args) || server.args.some((arg) => typeof arg !== 'string' || arg.includes('${')))) {
|
|
51
|
+
throw new Error('Claude MCP args must be strings without unresolved variable expansion.');
|
|
52
|
+
}
|
|
53
|
+
if (server.env !== undefined && (!isRecord(server.env) || Object.values(server.env).some((value) => typeof value !== 'string'))) {
|
|
54
|
+
throw new Error('Claude MCP env must contain string values.');
|
|
55
|
+
}
|
|
56
|
+
// Reference names are safe metadata; do not fingerprint env values or defaults.
|
|
57
|
+
adapter.envReferences = Object.fromEntries(Object.entries(server.env || {})
|
|
58
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
59
|
+
.map(([key, value]) => [key, environmentReferences(value)]));
|
|
60
|
+
} else {
|
|
61
|
+
if (typeof server.url !== 'string' || !server.url.trim() || server.url.includes('${')) {
|
|
62
|
+
throw new Error('Claude remote MCP servers require a literal HTTP(S) URL.');
|
|
63
|
+
}
|
|
64
|
+
let endpoint;
|
|
65
|
+
try { endpoint = new URL(server.url); } catch {
|
|
66
|
+
throw new Error('Invalid Claude MCP URL.');
|
|
67
|
+
}
|
|
68
|
+
if (!['https:', 'http:'].includes(endpoint.protocol)) {
|
|
69
|
+
throw new Error('Unsupported Claude MCP URL scheme.');
|
|
70
|
+
}
|
|
71
|
+
// The generic inspector fingerprints its input URL. Reject URL credentials
|
|
72
|
+
// and query/fragment values before they can enter that fingerprint.
|
|
73
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
74
|
+
throw new Error('Claude MCP URL credentials, query parameters, and fragments are unsupported; use headers.');
|
|
75
|
+
}
|
|
76
|
+
if (server.headers !== undefined) {
|
|
77
|
+
if (!isRecord(server.headers) || Object.values(server.headers).some((value) => typeof value !== 'string')) {
|
|
78
|
+
throw new Error('Claude MCP headers must contain string values.');
|
|
79
|
+
}
|
|
80
|
+
adapter.headerNames = Object.keys(server.headers).sort();
|
|
81
|
+
adapter.headerReferences = Object.fromEntries(Object.entries(server.headers)
|
|
82
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
83
|
+
.map(([key, value]) => [key, environmentReferences(value)]));
|
|
84
|
+
if (adapter.headerNames.length) {
|
|
85
|
+
const references = Object.values(adapter.headerReferences);
|
|
86
|
+
if (references.some((refs) => refs.length)) signals.push('env-http-headers');
|
|
87
|
+
if (references.some((refs) => !refs.length)) signals.push('static-http-headers');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (server.headersHelper !== undefined) {
|
|
91
|
+
if (typeof server.headersHelper !== 'string' || !server.headersHelper.trim()) {
|
|
92
|
+
throw new Error('Claude MCP headersHelper must be a non-empty string.');
|
|
93
|
+
}
|
|
94
|
+
signals.push('header-helper-command');
|
|
95
|
+
adapter.hasHeadersHelper = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return [name, {
|
|
100
|
+
...server,
|
|
101
|
+
_trustdexSignals: signals,
|
|
102
|
+
_trustdexFingerprintData: adapter
|
|
103
|
+
}];
|
|
104
|
+
});
|
|
105
|
+
return { mcpServers: Object.fromEntries(entries) };
|
|
106
|
+
}
|