@socketsecurity/mcp 0.0.20 → 0.2.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/CHANGELOG.md +56 -1
- package/README.md +276 -115
- package/dist/index.cjs +54907 -41297
- package/{hooks → dist}/socket-gate/README.md +13 -13
- package/dist/socket-gate/socket-gate.cjs +5391 -0
- package/package.json +185 -46
- package/hooks/socket-gate/index.mts +0 -296
- package/hooks/socket-gate/socket-gate.cjs +0 -1047
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@socketsecurity/mcp",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Socket MCP server for scanning dependencies",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/SocketDev/socket-mcp#readme",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"CHANGELOG.md",
|
|
20
20
|
"dist/index.cjs",
|
|
21
|
-
"
|
|
21
|
+
"dist/socket-gate",
|
|
22
22
|
"package.json"
|
|
23
23
|
],
|
|
24
24
|
"type": "module",
|
|
@@ -28,81 +28,220 @@
|
|
|
28
28
|
"provenance": true
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@anthropic-ai/mcpb": "
|
|
32
|
-
"@
|
|
33
|
-
"@
|
|
34
|
-
"@
|
|
35
|
-
"@
|
|
36
|
-
"@
|
|
37
|
-
"@
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
31
|
+
"@anthropic-ai/mcpb": "1.2.0",
|
|
32
|
+
"@bcoe/v8-coverage": "1.0.2",
|
|
33
|
+
"@mdn/browser-compat-data": "8.1.0",
|
|
34
|
+
"@modelcontextprotocol/client": "2.0.0",
|
|
35
|
+
"@modelcontextprotocol/core": "2.0.0",
|
|
36
|
+
"@modelcontextprotocol/node": "2.0.0",
|
|
37
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
38
|
+
"@playwright/mcp": "0.0.80",
|
|
39
|
+
"@shadscan/cli": "0.17.0",
|
|
40
|
+
"@sinclair/typebox": "0.34.52",
|
|
41
|
+
"@socketregistry/packageurl-js": "1.5.2",
|
|
42
|
+
"@socketregistry/packageurl-js-stable": "npm:@socketregistry/packageurl-js@1.5.2",
|
|
43
|
+
"@socketsecurity/lib": "7.0.1",
|
|
44
|
+
"@socketsecurity/lib-stable": "npm:@socketsecurity/lib@7.0.1",
|
|
45
|
+
"@socketsecurity/sdk": "4.1.4",
|
|
46
|
+
"@socketsecurity/sdk-stable": "npm:@socketsecurity/sdk@4.1.4",
|
|
47
|
+
"@types/mdast": "4.0.4",
|
|
48
|
+
"@types/node": "26.4.1",
|
|
49
|
+
"@types/semver": "7.8.0",
|
|
42
50
|
"@types/shell-quote": "1.7.5",
|
|
43
51
|
"@typescript/native-preview": "7.0.0-dev.20260510.1",
|
|
44
|
-
"@
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
52
|
+
"@ultrathink/acorn.rs.wasm": "0.1.1",
|
|
53
|
+
"@vitest/coverage-v8": "5.0.0",
|
|
54
|
+
"@vitest/ui": "5.0.0",
|
|
55
|
+
"@vitiate/core": "0.3.1",
|
|
56
|
+
"ast-v8-to-istanbul": "1.0.5",
|
|
57
|
+
"c8": "12.0.0",
|
|
58
|
+
"chrome-devtools-mcp": "1.8.0",
|
|
59
|
+
"fast-check": "4.9.0",
|
|
60
|
+
"magic-string": "1.2.3",
|
|
61
|
+
"markdownlint-cli2": "0.23.2",
|
|
62
|
+
"mcp-tada": "0.1.0",
|
|
63
|
+
"mdast-util-from-markdown": "2.0.3",
|
|
64
|
+
"mdast-util-gfm": "3.1.0",
|
|
65
|
+
"mdast-util-to-markdown": "2.1.2",
|
|
66
|
+
"micromark": "4.0.2",
|
|
67
|
+
"micromark-extension-gfm": "3.0.0",
|
|
68
|
+
"neosanitize": "0.3.0",
|
|
69
|
+
"nock": "14.0.17",
|
|
70
|
+
"npm-high-impact": "1.13.0",
|
|
71
|
+
"oxfmt": "0.66.0",
|
|
72
|
+
"oxlint": "1.81.0",
|
|
73
|
+
"oxlint-tsgolint": "7.0.2001",
|
|
74
|
+
"parse5": "8.0.1",
|
|
75
|
+
"playwright-core": "1.63.0",
|
|
76
|
+
"portless": "0.15.6",
|
|
77
|
+
"regjsparser": "0.13.2",
|
|
78
|
+
"rolldown": "1.2.7",
|
|
79
|
+
"run-local-ci": "0.18.1",
|
|
80
|
+
"semver": "7.8.5",
|
|
81
|
+
"shell-quote": "1.10.0",
|
|
82
|
+
"svgo": "4.1.0",
|
|
83
|
+
"taze": "21.1.0",
|
|
84
|
+
"typebox": "1.3.27",
|
|
85
|
+
"typescript": "7.1.0-dev.20260905.1",
|
|
86
|
+
"untracked": "1.6.5",
|
|
87
|
+
"vitest": "5.0.0",
|
|
88
|
+
"vitiate": "0.3.1",
|
|
89
|
+
"yaml": "2.9.0"
|
|
90
|
+
},
|
|
91
|
+
"devEngines": {
|
|
92
|
+
"packageManager": [
|
|
93
|
+
{
|
|
94
|
+
"name": "npm",
|
|
95
|
+
"version": "^11.19.0 || >=12.0.2",
|
|
96
|
+
"onFail": "error"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "pnpm",
|
|
100
|
+
"version": "^11.25.0 || >=12.3.4",
|
|
101
|
+
"onFail": "error"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
55
104
|
},
|
|
56
105
|
"engines": {
|
|
57
|
-
"node": ">=
|
|
58
|
-
"npm": "
|
|
106
|
+
"node": ">=24",
|
|
107
|
+
"npm": "^11.19.0 || >=12.0.2",
|
|
108
|
+
"pnpm": "^11.25.0 || >=12.3.4"
|
|
59
109
|
},
|
|
60
110
|
"allowScripts": {
|
|
61
111
|
"cpu-features": false,
|
|
62
112
|
"esbuild": true,
|
|
63
113
|
"protobufjs": false,
|
|
114
|
+
"puppeteer": false,
|
|
64
115
|
"ssh2": false
|
|
65
116
|
},
|
|
66
|
-
"npm-run-all2": {
|
|
67
|
-
"nodeRun": true
|
|
68
|
-
},
|
|
69
117
|
"scripts": {
|
|
118
|
+
"npm:publish": "node scripts/fleet/publish-pipeline.mts",
|
|
70
119
|
"test": "node scripts/fleet/test.mts",
|
|
71
|
-
"test:e2e": "
|
|
120
|
+
"test:e2e": "node scripts/fleet/test.mts test/repo/e2e",
|
|
121
|
+
"test:fuzz": "node scripts/repo/fuzz.mts",
|
|
72
122
|
"cover": "node scripts/fleet/cover.mts",
|
|
73
123
|
"test:tsc": "tsgo --noEmit -p tsconfig.json",
|
|
74
124
|
"lint": "node scripts/fleet/lint.mts",
|
|
75
125
|
"lint:all": "node scripts/fleet/lint.mts --all",
|
|
76
|
-
"format": "
|
|
77
|
-
"format:check": "
|
|
126
|
+
"format": "node scripts/fleet/format.mts",
|
|
127
|
+
"format:check": "node scripts/fleet/format.mts --check",
|
|
78
128
|
"check": "node scripts/fleet/check.mts",
|
|
79
129
|
"check:all": "node scripts/fleet/check.mts --all",
|
|
80
130
|
"fix": "node scripts/fleet/fix.mts",
|
|
81
131
|
"fix:all": "node scripts/fleet/fix.mts --all",
|
|
82
132
|
"update": "node scripts/fleet/update.mts",
|
|
83
|
-
"build": "
|
|
84
|
-
"build:bundle": "node scripts/build.mts",
|
|
133
|
+
"build": "pnpm run clean && pnpm run build:bundle && pnpm run build:permissions",
|
|
134
|
+
"build:bundle": "node scripts/repo/build.mts",
|
|
85
135
|
"build:permissions": "chmod +x ./dist/index.cjs",
|
|
86
|
-
"build-mcpb": "run-
|
|
87
|
-
"build-mcpb:versions_match": "node scripts/check-versions.
|
|
136
|
+
"build-mcpb": "pnpm run build && pnpm run build-mcpb:versions_match && pnpm run build-mcpb:validate && pnpm run build-mcpb:ensure-deps && pnpm run build-mcpb:mcpb-pack",
|
|
137
|
+
"build-mcpb:versions_match": "node scripts/repo/check-versions.mts",
|
|
88
138
|
"build-mcpb:validate": "mcpb validate ./",
|
|
89
139
|
"build-mcpb:ensure-deps": "npm install --production --ignore-scripts",
|
|
90
140
|
"build-mcpb:mcpb-pack": "mcpb pack ./",
|
|
91
|
-
"clean": "
|
|
92
|
-
"debug-stdio": "node ./mock-client/debug-client.
|
|
93
|
-
"debug-sdk": "node ./mock-client/stdio-client.
|
|
94
|
-
"debug-http": "node ./mock-client/http-client.
|
|
95
|
-
"server-stdio": "SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node ./index.
|
|
96
|
-
"server-stdio:debug": "SOCKET_DEBUG=1 SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node ./index.
|
|
97
|
-
"server-http": "MCP_HTTP_MODE=true SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node ./index.
|
|
98
|
-
"server-http:debug": "SOCKET_DEBUG=1 MCP_HTTP_MODE=true SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node ./index.
|
|
141
|
+
"clean": "node scripts/fleet/clean.mts",
|
|
142
|
+
"debug-stdio": "node ./mock-client/debug-client.mts",
|
|
143
|
+
"debug-sdk": "node ./mock-client/stdio-client.mts",
|
|
144
|
+
"debug-http": "node ./mock-client/http-client.mts",
|
|
145
|
+
"server-stdio": "pnpm run build:bundle && SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node ./dist/index.cjs",
|
|
146
|
+
"server-stdio:debug": "pnpm run build:bundle && SOCKET_DEBUG=1 SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node ./dist/index.cjs",
|
|
147
|
+
"server-http": "pnpm run build:bundle && MCP_HTTP_MODE=true SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node ./dist/index.cjs",
|
|
148
|
+
"server-http:debug": "pnpm run build:bundle && SOCKET_DEBUG=1 MCP_HTTP_MODE=true SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node ./dist/index.cjs",
|
|
99
149
|
"check:paths": "node scripts/fleet/check/paths-are-canonical.mts",
|
|
100
150
|
"security": "node scripts/fleet/security.mts",
|
|
101
151
|
"setup-security-tools": "node .claude/hooks/fleet/setup-security-tools/install.mts",
|
|
102
152
|
"lockstep": "node scripts/fleet/lockstep.mts",
|
|
103
153
|
"lockstep:emit-schema": "node scripts/fleet/lockstep-emit-schema.mts",
|
|
104
|
-
"
|
|
154
|
+
"lockstep:emit-mirror-globs": "node scripts/fleet/lockstep-emit-mirror-globs.mts",
|
|
155
|
+
"ci:local": "node scripts/fleet/ci/local/run.mts run --all --quiet --pause-on-failure --github-token",
|
|
105
156
|
"weekly-update": "node scripts/fleet/weekly-update.mts",
|
|
106
|
-
"
|
|
157
|
+
"doctor:auth": "node scripts/fleet/check/setup-is-prompt-less.mts",
|
|
158
|
+
"sync-oxlint-rules": "node scripts/fleet/sync-oxlint-rules.mts",
|
|
159
|
+
"sync-package-manager-pins": "node scripts/fleet/sync-package-manager-pins.mts",
|
|
160
|
+
"doctor": "node scripts/fleet/doctor.mts",
|
|
161
|
+
"setup:brew": "node scripts/fleet/setup/brew.mts",
|
|
162
|
+
"setup:go": "node scripts/fleet/setup/go.mts",
|
|
163
|
+
"setup:python": "node scripts/fleet/setup/python.mts",
|
|
164
|
+
"setup:refero": "node scripts/fleet/setup/refero.mts",
|
|
165
|
+
"setup:rust": "node scripts/fleet/setup/rust.mts",
|
|
166
|
+
"type": "node scripts/fleet/type.mts",
|
|
167
|
+
"socket-wheelhouse:emit-schema": "node scripts/fleet/socket-wheelhouse-emit-schema.mts",
|
|
168
|
+
"mcp:reset": "node scripts/fleet/mcp/reset.mts",
|
|
169
|
+
"setup:sfw-ca": "node scripts/fleet/setup/sfw-ca.mts",
|
|
170
|
+
"get-green": "node scripts/fleet/get-green.mts",
|
|
171
|
+
"weekly-update:ci": "gh workflow run weekly-update.yml",
|
|
172
|
+
"prune:backups": "node scripts/fleet/backup-branches.mts prune",
|
|
173
|
+
"strip:ai-tags": "node scripts/fleet/strip-ai-tags.mts",
|
|
174
|
+
"npm:approve": "node scripts/fleet/publish-pipeline.mts --approve",
|
|
175
|
+
"npm:auth": "node scripts/fleet/npm-auth.mts",
|
|
176
|
+
"npm:dispatch": "node scripts/fleet/registry-infra/remote-npm-publish.mts --publish",
|
|
177
|
+
"npm:staged": "node scripts/fleet/publish-pipeline.mts --status",
|
|
178
|
+
"hide-comments": "node scripts/fleet/hide-comments.mts",
|
|
179
|
+
"ruleset:bypass": "node scripts/fleet/grant-ruleset-bypass.mts",
|
|
180
|
+
"soak:bypass": "node scripts/fleet/soak-bypass.mts",
|
|
181
|
+
"npm:trust": "node scripts/fleet/registry-infra/npm/settings/trust.mts",
|
|
182
|
+
"prune:branch-backups": "node scripts/fleet/backup-branches.mts prune",
|
|
183
|
+
"preflight": "node scripts/repo/bootstrap/fleet.mjs --quiet && node scripts/fleet/preflight.mts",
|
|
184
|
+
"npm:auth:browser": "node scripts/fleet/npm-auth-browser.mts",
|
|
185
|
+
"npm:auth:cli": "node scripts/fleet/npm-auth-cli.mts",
|
|
186
|
+
"prune:gha-caches": "node scripts/fleet/prune-actions-caches.mts",
|
|
187
|
+
"check:fleet": "node scripts/fleet/check.mts",
|
|
188
|
+
"fix:js": "node scripts/fleet/fix.mts --js",
|
|
189
|
+
"fix:repo": "node scripts/fleet/fix.mts --repo",
|
|
190
|
+
"fmt:js": "node scripts/fleet/format.mts",
|
|
191
|
+
"lint:js": "node scripts/fleet/lint.mts",
|
|
192
|
+
"gh:auth": "node scripts/fleet/gh-auth.mts",
|
|
193
|
+
"sync-global-hooks": "node scripts/fleet/sync-global-hooks.mts",
|
|
194
|
+
"backup": "node scripts/fleet/backup-work.mts",
|
|
195
|
+
"offload:model": "node scripts/fleet/offload-model.mts",
|
|
196
|
+
"setup:offload-providers": "node scripts/fleet/setup/offload-providers.mts",
|
|
197
|
+
"build:hook-bundle": "node scripts/fleet/build-hook-bundle.mts",
|
|
198
|
+
"build:hook-snapshot": "node scripts/fleet/build-hook-snapshot.mts",
|
|
199
|
+
"build:snapshot-launcher": "node scripts/fleet/build-snapshot-launcher.mts",
|
|
200
|
+
"gen:glyph": "node scripts/fleet/gen/glyph.mts",
|
|
201
|
+
"mcp:schema:update": "mcp-tada introspect --command node --arg index.mts --env SOCKET_API_TOKEN=YOUR_API_TOKEN --env MCP_HTTP_MODE=false --timeout 5000 --json --out test/integration/server-schema.golden.json && pnpm run format test/integration/server-schema.golden.json",
|
|
202
|
+
"mcp:schema:check": "mcp-tada check --command node --arg index.mts --env SOCKET_API_TOKEN=YOUR_API_TOKEN --env MCP_HTTP_MODE=false --timeout 5000 --against test/integration/server-schema.golden.json",
|
|
203
|
+
"mcp:introspect": "node scripts/fleet/mcp/schemas.mts introspect",
|
|
204
|
+
"mcp:check": "node scripts/fleet/mcp/schemas.mts check",
|
|
205
|
+
"cover:types": "node scripts/fleet/cover/types/run.mts",
|
|
206
|
+
"audit:consumer-source": "node scripts/fleet/audit/consumer-source.mts",
|
|
207
|
+
"ai-balancer": "node scripts/fleet/ai/balancer/launch.mts",
|
|
208
|
+
"ai-balancer:auth": "node scripts/fleet/ai/balancer/login.mts",
|
|
209
|
+
"ai-balancer:refresh": "node scripts/fleet/ai/balancer/service.mts refresh",
|
|
210
|
+
"ai-balancer:service": "node scripts/fleet/ai/balancer/service.mts",
|
|
211
|
+
"ai:odai:cache": "node scripts/fleet/ai/odai/run.mts cache",
|
|
212
|
+
"ai:odai:diagnose": "node scripts/fleet/ai/odai/run.mts diagnose",
|
|
213
|
+
"ai:read": "node scripts/fleet/ai/bounded-reader.mts",
|
|
214
|
+
"spend-usage": "node scripts/fleet/spend-usage.mts",
|
|
215
|
+
"update:chrome": "node scripts/fleet/update/chrome.mts",
|
|
216
|
+
"build:oxlint-bundle": "node scripts/fleet/build-oxlint-bundle.mts",
|
|
217
|
+
"check:test-budget-headroom": "node scripts/fleet/check/test-budget-headroom.mts",
|
|
218
|
+
"check:workspace-installation": "node scripts/fleet/check/workspace-installation.mts",
|
|
219
|
+
"depot:auth": "node scripts/fleet/registry-infra/depot/browser-auth.mts",
|
|
220
|
+
"fleet:update": "node scripts/repo/bootstrap/fleet.mjs",
|
|
221
|
+
"integrity-refresh": "node scripts/fleet/external-tools/refresh.mts",
|
|
222
|
+
"setup:1password": "node scripts/fleet/setup/one-password.mts",
|
|
223
|
+
"setup:ci:local": "node scripts/fleet/ci/local/setup.mts",
|
|
224
|
+
"setup:ci:remote": "node scripts/fleet/ci/remote/setup.mts",
|
|
225
|
+
"setup:ai-clients": "node scripts/fleet/setup/ai-clients.mts",
|
|
226
|
+
"setup:pgbot": "node scripts/fleet/setup/pgbot.mts",
|
|
227
|
+
"mcp:connect": "node scripts/fleet/mcp/connect.mts",
|
|
228
|
+
"mcp:pgbot": "node scripts/fleet/mcp/pgbot/run.mts",
|
|
229
|
+
"mcp:schemas:check": "node scripts/fleet/mcp/schemas.mts check",
|
|
230
|
+
"mcp:schemas:save": "node scripts/fleet/mcp/schemas.mts save",
|
|
231
|
+
"mcp:status": "node scripts/fleet/mcp/connect.mts --status",
|
|
232
|
+
"npm:inventory": "node scripts/fleet/registry-infra/npm/account-inventory-read.mts",
|
|
233
|
+
"soak:check": "node scripts/fleet/check/soak-time-is-consistent.mts",
|
|
234
|
+
"sweep:worktrees": "node scripts/fleet/worktree-sweep.mts",
|
|
235
|
+
"sweep:worktrees:visit": "node scripts/fleet/worktree/sweep/run.mts",
|
|
236
|
+
"test:profile": "node scripts/fleet/test/profile.mts",
|
|
237
|
+
"update:pnpm": "node scripts/fleet/update/pnpm.mts",
|
|
238
|
+
"release:hint": "node scripts/fleet/release/hint.mts",
|
|
239
|
+
"slack:auth": "node scripts/fleet/mcp/connect.mts slack --client claude --reauthenticate",
|
|
240
|
+
"slack:token": "node scripts/fleet/setup/slack-token.mts",
|
|
241
|
+
"lockstep:assist": "node scripts/fleet/lockstep/assist/run.mts",
|
|
242
|
+
"mcp:1password": "node scripts/fleet/mcp/1password/run.mts",
|
|
243
|
+
"github:browser": "node scripts/fleet/browser/github-auth.mts",
|
|
244
|
+
"remote:github:release": "node scripts/fleet/release/github/remote.mts",
|
|
245
|
+
"worktree:create": "node scripts/fleet/worktree/create/run.mts"
|
|
107
246
|
}
|
|
108
247
|
}
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Socket-gate — Claude Code PreToolUse hook (source for the bundled
|
|
3
|
-
* socket-gate.cjs).
|
|
4
|
-
*
|
|
5
|
-
* Intercepts package install commands across npm, PyPI, Cargo, RubyGems, Go,
|
|
6
|
-
* and NuGet, and checks the target package against the public Socket MCP
|
|
7
|
-
* server. Blocks installs when the supply chain score is below 20 (known
|
|
8
|
-
* malware, typosquats, high-risk supply chain signals).
|
|
9
|
-
*
|
|
10
|
-
* No API key, no CLI, no registration beyond copying the bundled directory.
|
|
11
|
-
*
|
|
12
|
-
* Setup (see README.md):
|
|
13
|
-
*
|
|
14
|
-
* 1. Copy the whole hooks/socket-gate/ directory to ~/.claude/hooks/ (from a
|
|
15
|
-
* published @socketsecurity/mcp install, or build it here with `pnpm run
|
|
16
|
-
* build`).
|
|
17
|
-
* 2. Point a PreToolUse Bash hook at ~/.claude/hooks/socket-gate/socket-gate.cjs
|
|
18
|
-
* in ~/.claude/settings.json.
|
|
19
|
-
*
|
|
20
|
-
* This source imports @socketsecurity/lib-stable; rolldown inlines it into the
|
|
21
|
-
* bundled socket-gate.cjs so the copied-out hook stays self-contained (a Claude
|
|
22
|
-
* Code hook has no package.json and no node_modules — it can only run what is
|
|
23
|
-
* physically present in the file).
|
|
24
|
-
*
|
|
25
|
-
* Fails open on network, parse, and timeout errors so a Socket outage does not
|
|
26
|
-
* block legitimate work.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { readFileSync } from 'node:fs'
|
|
30
|
-
import { argv } from 'node:process'
|
|
31
|
-
import { fileURLToPath } from 'node:url'
|
|
32
|
-
|
|
33
|
-
import { errorMessage } from '@socketsecurity/lib-stable/errors'
|
|
34
|
-
|
|
35
|
-
const MCP_URL = 'https://mcp.socket.dev/'
|
|
36
|
-
const SUPPLY_CHAIN_THRESHOLD = 20
|
|
37
|
-
const REQUEST_TIMEOUT_MS = 10_000
|
|
38
|
-
|
|
39
|
-
export type Ecosystem = 'npm' | 'pypi' | 'cargo' | 'gem' | 'golang' | 'nuget'
|
|
40
|
-
|
|
41
|
-
export interface HookInput {
|
|
42
|
-
session_id: string
|
|
43
|
-
tool_name: string
|
|
44
|
-
tool_input: Record<string, unknown> | string
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Each pattern matches "<tool> <install-subcommand> <pkg>" and captures the
|
|
48
|
-
// first non-flag argument ([^\s-] rejects a leading dash, [^\s]* takes the
|
|
49
|
-
// rest) as the package spec to scan. The (?:…) groups list a tool's install
|
|
50
|
-
// aliases (npm add|i|install, cargo add|install, go get|install) without
|
|
51
|
-
// capturing them.
|
|
52
|
-
const INSTALL_PATTERNS: Array<{ ecosystem: Ecosystem; pattern: RegExp }> = [
|
|
53
|
-
{ ecosystem: 'npm', pattern: /\bnpm\s+(?:add|i|install)\s+([^\s-][^\s]*)/i }, // socket-lint: allow uncommented-regex
|
|
54
|
-
{ ecosystem: 'npm', pattern: /\byarn\s+add\s+([^\s-][^\s]*)/i },
|
|
55
|
-
{ ecosystem: 'npm', pattern: /\bpnpm\s+add\s+([^\s-][^\s]*)/i },
|
|
56
|
-
{ ecosystem: 'npm', pattern: /\bbun\s+add\s+([^\s-][^\s]*)/i },
|
|
57
|
-
{
|
|
58
|
-
ecosystem: 'pypi',
|
|
59
|
-
pattern: /(?:\bpython3?\s+-m\s+)?\bpip3?\s+install\s+([^\s-][^\s]*)/i, // socket-lint: allow uncommented-regex
|
|
60
|
-
},
|
|
61
|
-
{ ecosystem: 'pypi', pattern: /\buv\s+add\s+([^\s-][^\s]*)/i },
|
|
62
|
-
{ ecosystem: 'pypi', pattern: /\buv\s+pip\s+install\s+([^\s-][^\s]*)/i },
|
|
63
|
-
{ ecosystem: 'pypi', pattern: /\bpoetry\s+add\s+([^\s-][^\s]*)/i },
|
|
64
|
-
{ ecosystem: 'pypi', pattern: /\bpipenv\s+install\s+([^\s-][^\s]*)/i },
|
|
65
|
-
{
|
|
66
|
-
ecosystem: 'cargo',
|
|
67
|
-
pattern: /\bcargo\s+(?:add|install)\s+([^\s-][^\s]*)/i, // socket-lint: allow uncommented-regex
|
|
68
|
-
},
|
|
69
|
-
{ ecosystem: 'gem', pattern: /\bgem\s+install\s+([^\s-][^\s]*)/i },
|
|
70
|
-
{ ecosystem: 'gem', pattern: /\bbundle\s+add\s+([^\s-][^\s]*)/i },
|
|
71
|
-
{ ecosystem: 'golang', pattern: /\bgo\s+(?:get|install)\s+([^\s-][^\s]*)/i }, // socket-lint: allow uncommented-regex
|
|
72
|
-
{ ecosystem: 'nuget', pattern: /\bdotnet\s+add\s+package\s+([^\s-][^\s]*)/i },
|
|
73
|
-
{ ecosystem: 'nuget', pattern: /\bnuget\s+install\s+([^\s-][^\s]*)/i },
|
|
74
|
-
]
|
|
75
|
-
|
|
76
|
-
export async function checkPackage(
|
|
77
|
-
ecosystem: Ecosystem,
|
|
78
|
-
packageName: string,
|
|
79
|
-
fetchImpl: typeof fetch = fetch,
|
|
80
|
-
): Promise<{ decision: 'allow' | 'deny'; reason: string }> {
|
|
81
|
-
const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
82
|
-
const commonHeaders = {
|
|
83
|
-
'Content-Type': 'application/json',
|
|
84
|
-
Accept: 'application/json, text/event-stream',
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const initRes = await fetchImpl(MCP_URL, {
|
|
88
|
-
method: 'POST',
|
|
89
|
-
headers: commonHeaders,
|
|
90
|
-
body: JSON.stringify({
|
|
91
|
-
jsonrpc: '2.0',
|
|
92
|
-
id: 1,
|
|
93
|
-
method: 'initialize',
|
|
94
|
-
params: {
|
|
95
|
-
protocolVersion: '2024-11-05',
|
|
96
|
-
capabilities: {},
|
|
97
|
-
clientInfo: { name: 'socket-gate', version: '1.0' },
|
|
98
|
-
},
|
|
99
|
-
}),
|
|
100
|
-
signal,
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
if (!initRes.ok) {
|
|
104
|
-
throw new Error(`Socket MCP initialize returned ${initRes.status}`)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const sessionId = initRes.headers.get('mcp-session-id')
|
|
108
|
-
if (!sessionId) {
|
|
109
|
-
throw new Error('Socket MCP did not return a session id')
|
|
110
|
-
}
|
|
111
|
-
await initRes.text()
|
|
112
|
-
|
|
113
|
-
const callRes = await fetchImpl(MCP_URL, {
|
|
114
|
-
method: 'POST',
|
|
115
|
-
headers: { ...commonHeaders, 'Mcp-Session-Id': sessionId },
|
|
116
|
-
body: JSON.stringify({
|
|
117
|
-
jsonrpc: '2.0',
|
|
118
|
-
id: 2,
|
|
119
|
-
method: 'tools/call',
|
|
120
|
-
params: {
|
|
121
|
-
name: 'depscore',
|
|
122
|
-
arguments: {
|
|
123
|
-
packages: [{ ecosystem, depname: packageName }],
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
}),
|
|
127
|
-
signal,
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
if (!callRes.ok) {
|
|
131
|
-
throw new Error(`Socket MCP depscore returned ${callRes.status}`)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const payload = (await callRes.json()) as {
|
|
135
|
-
result?:
|
|
136
|
-
| {
|
|
137
|
-
content?: Array<{ type: string; text: string }> | undefined
|
|
138
|
-
isError?: boolean | undefined
|
|
139
|
-
}
|
|
140
|
-
| undefined
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (payload.result?.isError) {
|
|
144
|
-
throw new Error(
|
|
145
|
-
'Socket MCP reported a tool error (package likely not found)',
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const text = payload.result?.content?.[0]?.text ?? ''
|
|
150
|
-
const score = parseSupplyChainScore(text)
|
|
151
|
-
|
|
152
|
-
if (score === undefined) {
|
|
153
|
-
throw new Error('Could not parse supplyChain score from MCP response')
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (score < SUPPLY_CHAIN_THRESHOLD) {
|
|
157
|
-
return {
|
|
158
|
-
decision: 'deny',
|
|
159
|
-
reason: `Socket blocked "${packageName}" (${ecosystem}): supply chain score is ${score} (threshold ${SUPPLY_CHAIN_THRESHOLD}).\n\nReview: https://socket.dev/${ecosystem}/package/${packageName}`,
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return { decision: 'allow', reason: '' }
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function extractPackage(
|
|
167
|
-
command: string,
|
|
168
|
-
): { ecosystem: Ecosystem; name: string } | undefined {
|
|
169
|
-
for (const { ecosystem, pattern } of INSTALL_PATTERNS) {
|
|
170
|
-
const match = command.match(pattern)
|
|
171
|
-
if (match?.[1]) {
|
|
172
|
-
const name = stripVersion(match[1], ecosystem)
|
|
173
|
-
if (!name) {
|
|
174
|
-
continue
|
|
175
|
-
}
|
|
176
|
-
return { ecosystem, name }
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return undefined
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// stdout is the Claude Code hook IPC channel — the harness parses this exact
|
|
183
|
-
// JSON as the permission decision, so these must be raw writes (a logger would
|
|
184
|
-
// add formatting/timestamps and break the protocol).
|
|
185
|
-
export function outputAllow(): void {
|
|
186
|
-
const payload = JSON.stringify({
|
|
187
|
-
hookSpecificOutput: {
|
|
188
|
-
hookEventName: 'PreToolUse',
|
|
189
|
-
permissionDecision: 'allow',
|
|
190
|
-
},
|
|
191
|
-
})
|
|
192
|
-
process.stdout.write(payload) // socket-lint: allow (process-stdio: stdout is the hook decision protocol; raw write keeps the bundled hook free of logger indirection)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function outputDeny(reason: string): void {
|
|
196
|
-
const payload = JSON.stringify({
|
|
197
|
-
hookSpecificOutput: {
|
|
198
|
-
hookEventName: 'PreToolUse',
|
|
199
|
-
permissionDecision: 'deny',
|
|
200
|
-
permissionDecisionReason: reason,
|
|
201
|
-
},
|
|
202
|
-
})
|
|
203
|
-
process.stdout.write(payload) // socket-lint: allow (process-stdio: stdout is the hook decision protocol; raw write keeps the bundled hook free of logger indirection)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function parseSupplyChainScore(text: string): number | undefined {
|
|
207
|
-
// "supplyChain:" then optional spaces, capturing an integer with an
|
|
208
|
-
// optional ".<fraction>" decimal part (e.g. "supplyChain: 0.82").
|
|
209
|
-
const match = text.match(/supplyChain:\s*(\d+(?:\.\d+)?)/i)
|
|
210
|
-
return match ? Number(match[1]) : undefined
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function stripVersion(pkg: string, ecosystem: Ecosystem): string {
|
|
214
|
-
switch (ecosystem) {
|
|
215
|
-
case 'npm':
|
|
216
|
-
return pkg.replace(/@[\d^~].*/u, '').replace(/@latest$/u, '')
|
|
217
|
-
case 'pypi':
|
|
218
|
-
return pkg.split(/[=<>!~[]/)[0] ?? pkg
|
|
219
|
-
case 'cargo':
|
|
220
|
-
case 'golang':
|
|
221
|
-
case 'nuget':
|
|
222
|
-
return pkg.replace(/@.*$/u, '')
|
|
223
|
-
default:
|
|
224
|
-
return pkg
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function main(): Promise<void> {
|
|
229
|
-
let raw: string
|
|
230
|
-
try {
|
|
231
|
-
raw = readFileSync(0, 'utf-8')
|
|
232
|
-
} catch {
|
|
233
|
-
outputAllow()
|
|
234
|
-
return
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (!raw.trim()) {
|
|
238
|
-
outputAllow()
|
|
239
|
-
return
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
let input: HookInput
|
|
243
|
-
try {
|
|
244
|
-
input = JSON.parse(raw)
|
|
245
|
-
} catch {
|
|
246
|
-
outputAllow()
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (input.tool_name !== 'Bash') {
|
|
251
|
-
outputAllow()
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const command =
|
|
256
|
-
typeof input.tool_input === 'string'
|
|
257
|
-
? input.tool_input
|
|
258
|
-
: (input.tool_input?.['command'] as string) || ''
|
|
259
|
-
|
|
260
|
-
if (!command) {
|
|
261
|
-
outputAllow()
|
|
262
|
-
return
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const target = extractPackage(command)
|
|
266
|
-
if (!target) {
|
|
267
|
-
outputAllow()
|
|
268
|
-
return
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
const result = await checkPackage(target.ecosystem, target.name)
|
|
273
|
-
if (result.decision === 'deny') {
|
|
274
|
-
outputDeny(result.reason)
|
|
275
|
-
} else {
|
|
276
|
-
outputAllow()
|
|
277
|
-
}
|
|
278
|
-
} catch (e) {
|
|
279
|
-
// Deliberate fail-OPEN: a Socket outage / network error / parse failure
|
|
280
|
-
// must not block legitimate installs (this hook is an advisory guardrail,
|
|
281
|
-
// not a hard gate — see the file header). Surface the error on stderr so
|
|
282
|
-
// the failure is observable; stdout stays the allow/deny IPC channel.
|
|
283
|
-
const errLine = `socket-gate: check failed for ${target.ecosystem}/${target.name}, failing open: ${errorMessage(e)}\n`
|
|
284
|
-
process.stderr.write(errLine) // socket-lint: allow (process-stdio: stderr feedback for the harness; raw write keeps the bundled hook free of logger indirection)
|
|
285
|
-
outputAllow()
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const isMainModule =
|
|
290
|
-
argv[1] !== undefined && fileURLToPath(import.meta.url) === argv[1]
|
|
291
|
-
|
|
292
|
-
if (isMainModule) {
|
|
293
|
-
main().catch(() => {
|
|
294
|
-
outputAllow()
|
|
295
|
-
})
|
|
296
|
-
}
|