trustdex 0.4.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/LICENSE +21 -0
- package/README.md +321 -0
- package/SECURITY.md +47 -0
- package/bin/trustdex.mjs +498 -0
- package/docs/ARCHITECTURE.md +43 -0
- package/docs/RELEASING.md +53 -0
- package/docs/ROADMAP.md +33 -0
- package/docs/THREAT_MODEL.md +47 -0
- package/docs/TRUST_STORE.md +89 -0
- package/package.json +47 -0
- package/src/canonical.mjs +17 -0
- package/src/codex-config.mjs +308 -0
- package/src/extensions.mjs +112 -0
- package/src/gate.mjs +39 -0
- package/src/inspect.mjs +121 -0
- package/src/policy-packs.mjs +87 -0
- package/src/policy.mjs +102 -0
- package/src/provenance-online.mjs +394 -0
- package/src/provenance.mjs +194 -0
- package/src/review.mjs +39 -0
- package/src/snapshot.mjs +76 -0
- package/src/trust-record.mjs +59 -0
package/src/inspect.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { sha256Json } from './canonical.mjs';
|
|
2
|
+
|
|
3
|
+
const SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|credential|auth)/i;
|
|
4
|
+
const SHELLS = new Set(['sh', 'bash', 'zsh', 'fish', 'cmd', 'powershell', 'pwsh']);
|
|
5
|
+
const RUNNERS = new Set(['npx', 'pnpx', 'bunx', 'uvx']);
|
|
6
|
+
|
|
7
|
+
function executableName(command = '') {
|
|
8
|
+
const name = String(command).trim().split(/[\\/]/).pop().toLowerCase();
|
|
9
|
+
return name.replace(/\.(?:bat|cmd|exe)$/i, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function firstPackageArg(command, args) {
|
|
13
|
+
const cmd = executableName(command);
|
|
14
|
+
if (!RUNNERS.has(cmd)) return null;
|
|
15
|
+
const values = Array.isArray(args) ? args.map(String) : [];
|
|
16
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
17
|
+
const arg = values[i];
|
|
18
|
+
if (arg === '--package' || arg === '-p') return values[i + 1] || null;
|
|
19
|
+
if (arg.startsWith('--package=')) return arg.slice('--package='.length) || null;
|
|
20
|
+
if (arg.startsWith('-p=')) return arg.slice('-p='.length) || null;
|
|
21
|
+
if (arg.startsWith('-')) continue;
|
|
22
|
+
return arg;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isPinnedPackage(spec) {
|
|
28
|
+
if (!spec) return false;
|
|
29
|
+
const value = String(spec).trim();
|
|
30
|
+
if (!value || /@(latest|next|canary|beta|alpha)$/i.test(value)) return false;
|
|
31
|
+
if (value.startsWith('@')) {
|
|
32
|
+
const slash = value.indexOf('/');
|
|
33
|
+
if (slash < 0) return false;
|
|
34
|
+
return value.indexOf('@', slash) > slash;
|
|
35
|
+
}
|
|
36
|
+
return value.lastIndexOf('@') > 0 || /==[^=\s]+$/.test(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function looksLikePath(value) {
|
|
40
|
+
if (typeof value !== 'string') return false;
|
|
41
|
+
return value.startsWith('/') || value.startsWith('~/') || value.startsWith('./') ||
|
|
42
|
+
value.startsWith('../') || /^[A-Za-z]:\\/.test(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function extractRemoteEndpoint(server) {
|
|
46
|
+
const candidate = server?.url || server?.endpoint;
|
|
47
|
+
if (!candidate) return null;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(String(candidate));
|
|
51
|
+
const protocol = url.protocol.toLowerCase();
|
|
52
|
+
return {
|
|
53
|
+
host: url.hostname.toLowerCase(),
|
|
54
|
+
origin: `${protocol}//${url.host.toLowerCase()}`,
|
|
55
|
+
protocol,
|
|
56
|
+
hasEmbeddedCredentials: Boolean(url.username || url.password),
|
|
57
|
+
hasQuery: Boolean(url.search)
|
|
58
|
+
};
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeServer(name, server = {}) {
|
|
65
|
+
const command = String(server.command || '');
|
|
66
|
+
const args = Array.isArray(server.args) ? server.args.map(String) : [];
|
|
67
|
+
const env = server.env && typeof server.env === 'object' ? server.env : {};
|
|
68
|
+
const envKeys = Object.keys(env).sort();
|
|
69
|
+
const packageSpec = firstPackageArg(command, args);
|
|
70
|
+
const remote = extractRemoteEndpoint(server);
|
|
71
|
+
const remoteHost = remote?.host || null;
|
|
72
|
+
const signals = new Set(Array.isArray(server._trustdexSignals) ? server._trustdexSignals : []);
|
|
73
|
+
|
|
74
|
+
if (remote) signals.add('network-endpoint');
|
|
75
|
+
if (remote && ['http:', 'ws:'].includes(remote.protocol)) signals.add('insecure-transport');
|
|
76
|
+
if (remote?.hasEmbeddedCredentials) signals.add('embedded-credentials');
|
|
77
|
+
if (remote?.hasQuery) signals.add('url-query-parameters');
|
|
78
|
+
if (SHELLS.has(executableName(command))) signals.add('shell-execution');
|
|
79
|
+
if (RUNNERS.has(executableName(command))) signals.add('install-on-run');
|
|
80
|
+
if (packageSpec && !isPinnedPackage(packageSpec)) signals.add('unbounded-version');
|
|
81
|
+
if (args.some(looksLikePath)) signals.add('filesystem-path');
|
|
82
|
+
if (envKeys.some((key) => SECRET_KEY_RE.test(key))) signals.add('secret-env');
|
|
83
|
+
|
|
84
|
+
let source = { type: 'unknown' };
|
|
85
|
+
if (remote) {
|
|
86
|
+
source = {
|
|
87
|
+
type: 'remote',
|
|
88
|
+
host: remoteHost,
|
|
89
|
+
origin: remote.origin,
|
|
90
|
+
protocol: remote.protocol
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else if (packageSpec) source = { type: 'package', runner: executableName(command), package: packageSpec, pinned: isPinnedPackage(packageSpec) };
|
|
94
|
+
else if (command) source = { type: 'local', command };
|
|
95
|
+
|
|
96
|
+
const fingerprint = sha256Json({
|
|
97
|
+
command,
|
|
98
|
+
args,
|
|
99
|
+
url: server.url || null,
|
|
100
|
+
endpoint: server.endpoint || null,
|
|
101
|
+
envKeys,
|
|
102
|
+
adapter: server._trustdexFingerprintData || null
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return { name, source, fingerprint, signals: [...signals].sort(), envKeys };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function inspectMcpConfig(config) {
|
|
109
|
+
const servers = config?.mcpServers && typeof config.mcpServers === 'object'
|
|
110
|
+
? config.mcpServers
|
|
111
|
+
: config?.servers && typeof config.servers === 'object'
|
|
112
|
+
? config.servers
|
|
113
|
+
: config;
|
|
114
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers)) {
|
|
115
|
+
throw new Error('Expected an MCP configuration object, or an object containing mcpServers/servers.');
|
|
116
|
+
}
|
|
117
|
+
return Object.entries(servers)
|
|
118
|
+
.filter(([, value]) => value && typeof value === 'object' && !Array.isArray(value))
|
|
119
|
+
.map(([name, server]) => normalizeServer(name, server))
|
|
120
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
121
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const BASE_SIGNALS = {
|
|
2
|
+
'shell-execution': 'block',
|
|
3
|
+
'secret-env': 'ask',
|
|
4
|
+
'wildcard-tool-scope': 'ask',
|
|
5
|
+
'sensitive-path-instructions': 'ask',
|
|
6
|
+
'shell-instructions': 'ask',
|
|
7
|
+
'install-instructions': 'ask',
|
|
8
|
+
'missing-version': 'ask',
|
|
9
|
+
'header-helper-command': 'block',
|
|
10
|
+
'static-http-headers': 'ask',
|
|
11
|
+
'env-http-headers': 'ask',
|
|
12
|
+
'env-forwarding': 'ask',
|
|
13
|
+
'auto-approve-tools': 'ask',
|
|
14
|
+
'embedded-credentials': 'block',
|
|
15
|
+
'insecure-transport': 'block',
|
|
16
|
+
'url-query-parameters': 'ask',
|
|
17
|
+
'artifact-version-mismatch': 'block'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const POLICY_PACKS = Object.freeze({
|
|
21
|
+
strict: Object.freeze({
|
|
22
|
+
version: 1,
|
|
23
|
+
unknownAction: 'block',
|
|
24
|
+
localAction: 'ask',
|
|
25
|
+
verifiedPublisherAction: 'allow',
|
|
26
|
+
sensitiveAction: 'ask',
|
|
27
|
+
unpinnedAction: 'block',
|
|
28
|
+
shellAction: 'block',
|
|
29
|
+
trustedPackages: [],
|
|
30
|
+
trustedRepositories: [],
|
|
31
|
+
allowedRemoteHosts: [],
|
|
32
|
+
signalActions: BASE_SIGNALS
|
|
33
|
+
}),
|
|
34
|
+
'official-first': Object.freeze({
|
|
35
|
+
version: 1,
|
|
36
|
+
unknownAction: 'block',
|
|
37
|
+
localAction: 'ask',
|
|
38
|
+
verifiedPublisherAction: 'allow',
|
|
39
|
+
sensitiveAction: 'ask',
|
|
40
|
+
unpinnedAction: 'ask',
|
|
41
|
+
shellAction: 'block',
|
|
42
|
+
trustedPackages: [],
|
|
43
|
+
trustedRepositories: [],
|
|
44
|
+
allowedRemoteHosts: [],
|
|
45
|
+
signalActions: BASE_SIGNALS,
|
|
46
|
+
note: 'ALLOW verified provenance assertions, ASK for local extensions, and BLOCK unknown third-party sources by default.'
|
|
47
|
+
}),
|
|
48
|
+
development: Object.freeze({
|
|
49
|
+
version: 1,
|
|
50
|
+
unknownAction: 'ask',
|
|
51
|
+
localAction: 'ask',
|
|
52
|
+
verifiedPublisherAction: 'allow',
|
|
53
|
+
sensitiveAction: 'ask',
|
|
54
|
+
unpinnedAction: 'ask',
|
|
55
|
+
shellAction: 'ask',
|
|
56
|
+
trustedPackages: [],
|
|
57
|
+
trustedRepositories: [],
|
|
58
|
+
allowedRemoteHosts: ['localhost', '127.0.0.1', '::1'],
|
|
59
|
+
signalActions: {
|
|
60
|
+
...BASE_SIGNALS,
|
|
61
|
+
'shell-execution': 'ask',
|
|
62
|
+
'insecure-transport': 'ask'
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export function getPolicyPack(name = 'strict') {
|
|
68
|
+
const pack = POLICY_PACKS[name];
|
|
69
|
+
if (!pack) {
|
|
70
|
+
throw new Error(`Unknown policy pack "${name}". Available: ${Object.keys(POLICY_PACKS).join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
return JSON.parse(JSON.stringify(pack));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function mergePolicy(base, overlay = {}) {
|
|
76
|
+
return {
|
|
77
|
+
...base,
|
|
78
|
+
...overlay,
|
|
79
|
+
signalActions: {
|
|
80
|
+
...(base.signalActions || {}),
|
|
81
|
+
...(overlay.signalActions || {})
|
|
82
|
+
},
|
|
83
|
+
trustedPackages: overlay.trustedPackages ?? base.trustedPackages ?? [],
|
|
84
|
+
trustedRepositories: overlay.trustedRepositories ?? base.trustedRepositories ?? [],
|
|
85
|
+
allowedRemoteHosts: overlay.allowedRemoteHosts ?? base.allowedRemoteHosts ?? []
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/policy.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const ACTIONS = new Set(['allow', 'ask', 'block']);
|
|
2
|
+
|
|
3
|
+
function normalizeAction(value, fallback) {
|
|
4
|
+
const action = String(value || fallback).toLowerCase();
|
|
5
|
+
return ACTIONS.has(action) ? action : fallback;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function packageName(spec = '') {
|
|
9
|
+
const value = String(spec);
|
|
10
|
+
if (value.startsWith('@')) {
|
|
11
|
+
const slash = value.indexOf('/');
|
|
12
|
+
if (slash < 0) return value;
|
|
13
|
+
const versionAt = value.indexOf('@', slash);
|
|
14
|
+
return versionAt > slash ? value.slice(0, versionAt) : value;
|
|
15
|
+
}
|
|
16
|
+
const versionAt = value.lastIndexOf('@');
|
|
17
|
+
return versionAt > 0 ? value.slice(0, versionAt) : value.replace(/==[^=\s]+$/, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function raise(current, next) {
|
|
21
|
+
const rank = { allow: 0, ask: 1, block: 2 };
|
|
22
|
+
return rank[next] > rank[current] ? next : current;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function signalAction(policy, signal, fallback = null) {
|
|
26
|
+
const configured = policy?.signalActions?.[signal];
|
|
27
|
+
if (!configured) return fallback;
|
|
28
|
+
return normalizeAction(configured, fallback || 'ask');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function evaluateServer(server, policy = {}) {
|
|
32
|
+
const unknownAction = normalizeAction(policy.unknownAction, 'block');
|
|
33
|
+
const localAction = normalizeAction(policy.localAction, 'ask');
|
|
34
|
+
const sensitiveAction = normalizeAction(policy.sensitiveAction, 'ask');
|
|
35
|
+
const unpinnedAction = normalizeAction(policy.unpinnedAction, 'ask');
|
|
36
|
+
const shellAction = normalizeAction(policy.shellAction, 'block');
|
|
37
|
+
const verifiedPublisherAction = normalizeAction(policy.verifiedPublisherAction, 'allow');
|
|
38
|
+
|
|
39
|
+
const trustedPackages = new Set(policy.trustedPackages || []);
|
|
40
|
+
const trustedRepositories = new Set((policy.trustedRepositories || []).map((x) => String(x).toLowerCase()));
|
|
41
|
+
const allowedRemoteHosts = new Set((policy.allowedRemoteHosts || []).map((x) => String(x).toLowerCase()));
|
|
42
|
+
|
|
43
|
+
let action = unknownAction;
|
|
44
|
+
const reasons = [];
|
|
45
|
+
|
|
46
|
+
if (server.provenance?.status === 'verified') {
|
|
47
|
+
action = verifiedPublisherAction;
|
|
48
|
+
reasons.push(`verified provenance assertion: ${server.provenance.publisher}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (server.source.type === 'package') {
|
|
52
|
+
const pkg = packageName(server.source.package);
|
|
53
|
+
if (trustedPackages.has(pkg)) {
|
|
54
|
+
action = 'allow';
|
|
55
|
+
reasons.push(`trusted package: ${pkg}`);
|
|
56
|
+
} else if (server.provenance?.status !== 'verified') {
|
|
57
|
+
reasons.push(`package is not trusted: ${pkg}`);
|
|
58
|
+
}
|
|
59
|
+
if (!server.source.pinned) {
|
|
60
|
+
action = raise(action, unpinnedAction);
|
|
61
|
+
reasons.push('package version is not pinned');
|
|
62
|
+
}
|
|
63
|
+
} else if (server.source.type === 'remote') {
|
|
64
|
+
if (allowedRemoteHosts.has(server.source.host)) {
|
|
65
|
+
action = 'allow';
|
|
66
|
+
reasons.push(`allowed remote host: ${server.source.host}`);
|
|
67
|
+
} else if (server.provenance?.status !== 'verified') {
|
|
68
|
+
reasons.push(`remote host is not allowlisted: ${server.source.host}`);
|
|
69
|
+
}
|
|
70
|
+
} else if (server.source.type === 'repository') {
|
|
71
|
+
const repository = String(server.source.repository || '').toLowerCase();
|
|
72
|
+
if (trustedRepositories.has(repository)) {
|
|
73
|
+
action = 'allow';
|
|
74
|
+
reasons.push(`trusted repository: ${server.source.repository}`);
|
|
75
|
+
} else if (server.provenance?.status !== 'verified') {
|
|
76
|
+
reasons.push(`repository is not trusted: ${server.source.repository || 'unknown'}`);
|
|
77
|
+
}
|
|
78
|
+
} else if (['local', 'skill-file', 'plugin-manifest'].includes(server.source.type)) {
|
|
79
|
+
if (server.provenance?.status !== 'verified') action = localAction;
|
|
80
|
+
reasons.push('local extension requires local trust policy');
|
|
81
|
+
} else if (server.provenance?.status !== 'verified') {
|
|
82
|
+
reasons.push('source could not be identified');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const signal of server.signals || []) {
|
|
86
|
+
let next = signalAction(policy, signal);
|
|
87
|
+
|
|
88
|
+
if (!next && signal === 'secret-env') next = sensitiveAction;
|
|
89
|
+
if (!next && signal === 'shell-execution') next = shellAction;
|
|
90
|
+
|
|
91
|
+
if (next) {
|
|
92
|
+
action = raise(action, next);
|
|
93
|
+
reasons.push(`signal policy: ${signal} -> ${next}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { ...server, action, reasons };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function evaluateAll(servers, policy = {}) {
|
|
101
|
+
return servers.map((server) => evaluateServer(server, policy));
|
|
102
|
+
}
|