surfscan 0.0.0-stage → 0.1.1
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 +19 -0
- package/LICENSE +21 -0
- package/README.md +188 -2
- package/dist/chunk-MJKZGRUS.js +2902 -0
- package/dist/chunk-MJKZGRUS.js.map +1 -0
- package/dist/cli/bin.js +514 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/index.d.ts +294 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/docs/rules.md +514 -0
- package/package.json +64 -4
package/dist/cli/bin.js
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CONFIG_FILE_NAME,
|
|
4
|
+
ConfigError,
|
|
5
|
+
DEFAULT_CONFIG,
|
|
6
|
+
LOCK_FILE_PATH,
|
|
7
|
+
SEVERITIES,
|
|
8
|
+
UNNAMED_SERVER,
|
|
9
|
+
approvalsScanner,
|
|
10
|
+
buildLock,
|
|
11
|
+
createNodeProvider,
|
|
12
|
+
depsScanner,
|
|
13
|
+
escapeUnsafe,
|
|
14
|
+
findMcpTargets,
|
|
15
|
+
formatJson,
|
|
16
|
+
formatText,
|
|
17
|
+
hasFailures,
|
|
18
|
+
keysScanner,
|
|
19
|
+
loadConfig,
|
|
20
|
+
mcpScanner,
|
|
21
|
+
parseLock,
|
|
22
|
+
parseMcpConfig,
|
|
23
|
+
parseToolList,
|
|
24
|
+
redactSecrets,
|
|
25
|
+
rulesScanner,
|
|
26
|
+
runScanners,
|
|
27
|
+
unpinnedTarget,
|
|
28
|
+
validateConfig
|
|
29
|
+
} from "../chunk-MJKZGRUS.js";
|
|
30
|
+
|
|
31
|
+
// src/cli/bin.ts
|
|
32
|
+
import { createInterface } from "readline/promises";
|
|
33
|
+
|
|
34
|
+
// src/cli/index.ts
|
|
35
|
+
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
36
|
+
import { dirname, join as join2, resolve } from "path";
|
|
37
|
+
import { Command, CommanderError, Option } from "commander";
|
|
38
|
+
|
|
39
|
+
// src/cli/connect.ts
|
|
40
|
+
import { spawn, spawnSync } from "child_process";
|
|
41
|
+
import { join } from "path";
|
|
42
|
+
var TIMEOUT_MS = 3e4;
|
|
43
|
+
var MAX_OUTPUT_CHARS = 16 * 1024 * 1024;
|
|
44
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
45
|
+
function expand(value, env) {
|
|
46
|
+
return value.replace(/\$\{(?:env:)?([A-Za-z_]\w*)\}/g, (whole, name) => env[name] ?? whole);
|
|
47
|
+
}
|
|
48
|
+
function winQuote(arg) {
|
|
49
|
+
return /[\s"&|<>^]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
|
|
50
|
+
}
|
|
51
|
+
var BASE_ENV_ALLOWLIST = new Set(
|
|
52
|
+
[
|
|
53
|
+
"PATH",
|
|
54
|
+
"PATHEXT",
|
|
55
|
+
"HOME",
|
|
56
|
+
"USERPROFILE",
|
|
57
|
+
"HOMEDRIVE",
|
|
58
|
+
"HOMEPATH",
|
|
59
|
+
"APPDATA",
|
|
60
|
+
"LOCALAPPDATA",
|
|
61
|
+
"TEMP",
|
|
62
|
+
"TMP",
|
|
63
|
+
"TMPDIR",
|
|
64
|
+
"SystemRoot",
|
|
65
|
+
"SystemDrive",
|
|
66
|
+
"WINDIR",
|
|
67
|
+
"ComSpec",
|
|
68
|
+
"ProgramFiles",
|
|
69
|
+
"ProgramFiles(x86)",
|
|
70
|
+
"ProgramData",
|
|
71
|
+
"USER",
|
|
72
|
+
"USERNAME",
|
|
73
|
+
"LOGNAME",
|
|
74
|
+
"SHELL",
|
|
75
|
+
"TERM",
|
|
76
|
+
"LANG",
|
|
77
|
+
"LC_ALL",
|
|
78
|
+
"LC_CTYPE",
|
|
79
|
+
"XDG_CONFIG_HOME",
|
|
80
|
+
"XDG_CACHE_HOME",
|
|
81
|
+
"XDG_DATA_HOME",
|
|
82
|
+
"HTTP_PROXY",
|
|
83
|
+
"HTTPS_PROXY",
|
|
84
|
+
"NO_PROXY",
|
|
85
|
+
"ALL_PROXY",
|
|
86
|
+
"SSL_CERT_FILE",
|
|
87
|
+
"SSL_CERT_DIR",
|
|
88
|
+
"NODE_EXTRA_CA_CERTS"
|
|
89
|
+
].map((name) => name.toLowerCase())
|
|
90
|
+
);
|
|
91
|
+
function buildChildEnv(server, shellEnv) {
|
|
92
|
+
const env = {};
|
|
93
|
+
for (const [key, value] of Object.entries(shellEnv)) {
|
|
94
|
+
if (value !== void 0 && BASE_ENV_ALLOWLIST.has(key.toLowerCase())) env[key] = value;
|
|
95
|
+
}
|
|
96
|
+
for (const [key, value] of Object.entries(server.env)) env[key] = expand(value, shellEnv);
|
|
97
|
+
return env;
|
|
98
|
+
}
|
|
99
|
+
function stopServer(child) {
|
|
100
|
+
child.stdin.destroy();
|
|
101
|
+
const systemRoot = process.env.SystemRoot;
|
|
102
|
+
if (process.platform === "win32" && child.pid !== void 0 && systemRoot) {
|
|
103
|
+
spawnSync(join(systemRoot, "System32", "taskkill.exe"), ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
104
|
+
}
|
|
105
|
+
child.kill();
|
|
106
|
+
child.stdout.destroy();
|
|
107
|
+
child.unref();
|
|
108
|
+
}
|
|
109
|
+
function listServerTools(server, baseEnv) {
|
|
110
|
+
return new Promise((resolve2, reject) => {
|
|
111
|
+
const env = buildChildEnv(server, baseEnv);
|
|
112
|
+
const command = server.command;
|
|
113
|
+
const args = server.args;
|
|
114
|
+
const windows = process.platform === "win32";
|
|
115
|
+
if (windows && [command, ...args].some((part) => /%[^%\s]+%/.test(part))) {
|
|
116
|
+
reject(new Error("%VAR% in a command or argument cannot be passed safely on Windows"));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const child = windows ? spawn([command, ...args].map(winQuote).join(" "), { env, shell: true, stdio: ["pipe", "pipe", "ignore"], windowsHide: true }) : spawn(command, args, { env, stdio: ["pipe", "pipe", "ignore"] });
|
|
120
|
+
const tools = [];
|
|
121
|
+
let buffer = "";
|
|
122
|
+
let received = 0;
|
|
123
|
+
let finished = false;
|
|
124
|
+
const finish = (err) => {
|
|
125
|
+
if (finished) return;
|
|
126
|
+
finished = true;
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
stopServer(child);
|
|
129
|
+
if (err) reject(err);
|
|
130
|
+
else resolve2(parseToolList(JSON.stringify({ tools }))[UNNAMED_SERVER]);
|
|
131
|
+
};
|
|
132
|
+
const timer = setTimeout(() => finish(new Error(`no tool list after ${TIMEOUT_MS / 1e3} seconds`)), TIMEOUT_MS);
|
|
133
|
+
const send = (message) => {
|
|
134
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}
|
|
135
|
+
`);
|
|
136
|
+
};
|
|
137
|
+
child.on("error", (err) => finish(err));
|
|
138
|
+
child.on("exit", (code2) => finish(new Error(`server exited early with code ${code2}`)));
|
|
139
|
+
child.stdin.on("error", () => void 0);
|
|
140
|
+
child.stdout.setEncoding("utf8");
|
|
141
|
+
child.stdout.on("data", (chunk) => {
|
|
142
|
+
received += chunk.length;
|
|
143
|
+
if (received > MAX_OUTPUT_CHARS) return finish(new Error("server sent too much output"));
|
|
144
|
+
buffer += chunk;
|
|
145
|
+
let newline;
|
|
146
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
147
|
+
const line = buffer.slice(0, newline).trim();
|
|
148
|
+
buffer = buffer.slice(newline + 1);
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(line);
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (typeof parsed !== "object" || parsed === null) continue;
|
|
156
|
+
const message = parsed;
|
|
157
|
+
if (message.error) return finish(new Error(String(message.error.message)));
|
|
158
|
+
if (message.id === 1) {
|
|
159
|
+
send({ method: "notifications/initialized" });
|
|
160
|
+
send({ id: 2, method: "tools/list", params: {} });
|
|
161
|
+
} else if (message.id === 2) {
|
|
162
|
+
if (Array.isArray(message.result?.tools)) for (const tool of message.result.tools) tools.push(tool);
|
|
163
|
+
const cursor = message.result?.nextCursor;
|
|
164
|
+
if (typeof cursor === "string" && cursor) send({ id: 2, method: "tools/list", params: { cursor } });
|
|
165
|
+
else return finish(void 0);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
send({
|
|
170
|
+
id: 1,
|
|
171
|
+
method: "initialize",
|
|
172
|
+
params: { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "surfscan", version: "0" } }
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function connectToServers(servers, env, warn) {
|
|
177
|
+
const tools = {};
|
|
178
|
+
for (const server of servers) {
|
|
179
|
+
if (!server.command) {
|
|
180
|
+
warn(`${server.name}: skipped, --connect only launches local servers, not remote URLs`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
tools[server.name] = await listServerTools(server, env);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
warn(`${server.name}: could not list tools (${err.message})`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return tools;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// package.json
|
|
193
|
+
var package_default = {
|
|
194
|
+
name: "surfscan",
|
|
195
|
+
version: "0.1.1",
|
|
196
|
+
description: "Attack surface scanner for AI-assisted development",
|
|
197
|
+
keywords: [
|
|
198
|
+
"ai",
|
|
199
|
+
"security",
|
|
200
|
+
"mcp",
|
|
201
|
+
"rag",
|
|
202
|
+
"prompt-injection",
|
|
203
|
+
"llm",
|
|
204
|
+
"scanner"
|
|
205
|
+
],
|
|
206
|
+
license: "MIT",
|
|
207
|
+
author: "Omogbai Atakpu",
|
|
208
|
+
repository: {
|
|
209
|
+
type: "git",
|
|
210
|
+
url: "git+https://github.com/githero20/surfscan.git"
|
|
211
|
+
},
|
|
212
|
+
bugs: {
|
|
213
|
+
url: "https://github.com/githero20/surfscan/issues"
|
|
214
|
+
},
|
|
215
|
+
homepage: "https://github.com/githero20/surfscan#readme",
|
|
216
|
+
type: "module",
|
|
217
|
+
bin: {
|
|
218
|
+
surfscan: "dist/cli/bin.js"
|
|
219
|
+
},
|
|
220
|
+
main: "dist/index.js",
|
|
221
|
+
types: "dist/index.d.ts",
|
|
222
|
+
exports: {
|
|
223
|
+
".": {
|
|
224
|
+
types: "./dist/index.d.ts",
|
|
225
|
+
import: "./dist/index.js"
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
files: [
|
|
229
|
+
"dist",
|
|
230
|
+
"README.md",
|
|
231
|
+
"LICENSE",
|
|
232
|
+
"CHANGELOG.md",
|
|
233
|
+
"docs"
|
|
234
|
+
],
|
|
235
|
+
engines: {
|
|
236
|
+
node: ">=20"
|
|
237
|
+
},
|
|
238
|
+
scripts: {
|
|
239
|
+
build: "tsup",
|
|
240
|
+
prepublishOnly: "npm run typecheck && npm test && npm run build",
|
|
241
|
+
typecheck: "tsc --noEmit",
|
|
242
|
+
test: "vitest run",
|
|
243
|
+
"test:watch": "vitest"
|
|
244
|
+
},
|
|
245
|
+
dependencies: {
|
|
246
|
+
commander: "^14.0.3",
|
|
247
|
+
"fast-glob": "^3.3.3"
|
|
248
|
+
},
|
|
249
|
+
devDependencies: {
|
|
250
|
+
"@types/node": "^22.20.5",
|
|
251
|
+
tsup: "^9.0.0",
|
|
252
|
+
typescript: "^5.9.3",
|
|
253
|
+
vitest: "^5.1.0"
|
|
254
|
+
},
|
|
255
|
+
allowScripts: {
|
|
256
|
+
"esbuild@0.28.2": true
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// src/cli/index.ts
|
|
261
|
+
var EXIT_OK = 0;
|
|
262
|
+
var EXIT_FINDINGS = 1;
|
|
263
|
+
var EXIT_ERROR = 2;
|
|
264
|
+
var ALL_SCANNERS = [rulesScanner, mcpScanner, depsScanner, keysScanner, approvalsScanner];
|
|
265
|
+
var COMMANDS = {
|
|
266
|
+
rules: { scanners: [rulesScanner], summary: "check agent instruction files for hidden or risky instructions" },
|
|
267
|
+
mcp: { scanners: [mcpScanner], summary: "audit MCP server configs and tool descriptions" },
|
|
268
|
+
deps: { scanners: [depsScanner], summary: "verify dependencies against the npm registry", network: true },
|
|
269
|
+
keys: { scanners: [keysScanner], summary: "find keys and secrets an agent can reach" },
|
|
270
|
+
approvals: { scanners: [approvalsScanner], summary: "find commands agents may run without asking" },
|
|
271
|
+
scan: { scanners: ALL_SCANNERS, summary: "run all scanners and merge the results", network: true }
|
|
272
|
+
};
|
|
273
|
+
var UsageError = class extends Error {
|
|
274
|
+
};
|
|
275
|
+
async function readIfExists(path) {
|
|
276
|
+
try {
|
|
277
|
+
return await readFile(path, "utf8");
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (err.code === "ENOENT") return void 0;
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function resolveRoot(cwd, pathArg) {
|
|
284
|
+
const root = resolve(cwd, pathArg ?? ".");
|
|
285
|
+
let isDir = false;
|
|
286
|
+
try {
|
|
287
|
+
isDir = (await stat(root)).isDirectory();
|
|
288
|
+
} catch {
|
|
289
|
+
throw new UsageError(`path not found: ${pathArg ?? "."}`);
|
|
290
|
+
}
|
|
291
|
+
if (!isDir) throw new UsageError(`not a directory: ${pathArg}`);
|
|
292
|
+
return root;
|
|
293
|
+
}
|
|
294
|
+
var safeText = (text) => escapeUnsafe(redactSecrets(text));
|
|
295
|
+
async function configuredServers(provider, config, io) {
|
|
296
|
+
const servers = [];
|
|
297
|
+
for (const path of await findMcpTargets(provider, config)) {
|
|
298
|
+
const content = await provider.read(path);
|
|
299
|
+
if (content === void 0) continue;
|
|
300
|
+
try {
|
|
301
|
+
servers.push(...parseMcpConfig({ path, content }));
|
|
302
|
+
} catch (err) {
|
|
303
|
+
io.stderr(`warning: ${safeText(`${path}: could not parse MCP config (${err.message})`)}
|
|
304
|
+
`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return servers;
|
|
308
|
+
}
|
|
309
|
+
function describeEnv(server) {
|
|
310
|
+
const names = Object.entries(server.env).map(([key, value]) => {
|
|
311
|
+
const fromShell = [...value.matchAll(/\$\{(?:env:)?([A-Za-z_]\w*)\}/g)].map((m) => `$${m[1]}`);
|
|
312
|
+
return fromShell.length > 0 ? `${key} (from your shell: ${fromShell.join(", ")})` : key;
|
|
313
|
+
});
|
|
314
|
+
return names.length > 0 ? names.join(", ") : "none";
|
|
315
|
+
}
|
|
316
|
+
async function connectSource(provider, config, opts, io) {
|
|
317
|
+
const servers = await configuredServers(provider, config, io);
|
|
318
|
+
const warnings = [];
|
|
319
|
+
const warn = (message) => {
|
|
320
|
+
warnings.push(message);
|
|
321
|
+
io.stderr(`warning: ${safeText(message)}
|
|
322
|
+
`);
|
|
323
|
+
};
|
|
324
|
+
let skippedUnpinned = false;
|
|
325
|
+
io.stderr("WARNING: --connect runs third-party code on this machine.\n");
|
|
326
|
+
io.stderr("Each server gets a minimal environment (PATH, HOME, temp folders, proxy and certificate settings)\n");
|
|
327
|
+
io.stderr("plus the variables listed under env. Your other shell variables are not passed on.\n\n");
|
|
328
|
+
const approved = [];
|
|
329
|
+
for (const s of servers) {
|
|
330
|
+
if (!s.command) {
|
|
331
|
+
warn(`${s.name}: skipped, --connect only launches local servers, not remote URLs`);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const target = unpinnedTarget(s);
|
|
335
|
+
io.stderr(`${safeText(s.name)}
|
|
336
|
+
runs: ${safeText([s.command, ...s.args].join(" "))}
|
|
337
|
+
env: ${safeText(describeEnv(s))}
|
|
338
|
+
`);
|
|
339
|
+
if (target) io.stderr(` NOT PINNED: it runs whatever is newest (${safeText(target)}), which may differ from what you reviewed
|
|
340
|
+
`);
|
|
341
|
+
if (/\$\{(?:env:)?[A-Za-z_]\w*\}/.test([s.command, ...s.args].join(" "))) {
|
|
342
|
+
io.stderr(" note: ${VAR} is only expanded in env, so here it is passed as written\n");
|
|
343
|
+
}
|
|
344
|
+
if (opts.yes) {
|
|
345
|
+
if (target && !opts.allowUnpinned) {
|
|
346
|
+
warn(`${s.name}: skipped because it is not pinned (pin a version, or pass --allow-unpinned)`);
|
|
347
|
+
skippedUnpinned = true;
|
|
348
|
+
} else {
|
|
349
|
+
approved.push(s);
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (!io.confirm) throw new UsageError("--connect needs confirmation; nothing was launched (pass --yes to skip the question)");
|
|
354
|
+
const ok = target ? await io.confirm(' Not pinned. Type "yes" to launch it anyway: ', { exact: "yes" }) : await io.confirm(" Launch this server? [y/N] ");
|
|
355
|
+
if (ok) approved.push(s);
|
|
356
|
+
else io.stderr(" skipped\n");
|
|
357
|
+
}
|
|
358
|
+
if (approved.length === 0) throw new UsageError("--connect: no servers were launched");
|
|
359
|
+
const tools = await connectToServers(approved, io.env, warn);
|
|
360
|
+
const incomplete = skippedUnpinned || approved.some((s) => !(s.name in tools));
|
|
361
|
+
return { source: { tools }, notChecked: servers.map((s) => s.name).filter((name) => !(name in tools)), warnings, incomplete };
|
|
362
|
+
}
|
|
363
|
+
async function loadToolSource(root, provider, config, opts, io) {
|
|
364
|
+
if (opts.tools && opts.connect) throw new UsageError("use either --tools or --connect, not both");
|
|
365
|
+
if (opts.updateLock && !opts.tools && !opts.connect) {
|
|
366
|
+
throw new UsageError("--update-lock needs a tool list: pass --tools <file.json> or --connect");
|
|
367
|
+
}
|
|
368
|
+
let source;
|
|
369
|
+
let notChecked = [];
|
|
370
|
+
let warnings = [];
|
|
371
|
+
let incomplete = false;
|
|
372
|
+
if (opts.connect) {
|
|
373
|
+
({ source, notChecked, warnings, incomplete } = await connectSource(provider, config, opts, io));
|
|
374
|
+
} else if (opts.tools) {
|
|
375
|
+
const content = await readIfExists(resolve(io.cwd, opts.tools));
|
|
376
|
+
if (content === void 0) throw new UsageError(`tool list not found: ${opts.tools}`);
|
|
377
|
+
try {
|
|
378
|
+
source = { file: opts.tools.replace(/\\/g, "/"), content, tools: parseToolList(content) };
|
|
379
|
+
} catch (err) {
|
|
380
|
+
throw new UsageError(`${opts.tools}: invalid tool list (${err.message})`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (source && opts.updateLock) {
|
|
384
|
+
const path = join2(root, LOCK_FILE_PATH);
|
|
385
|
+
await mkdir(dirname(path), { recursive: true });
|
|
386
|
+
const lock = await buildLock(source.tools);
|
|
387
|
+
const saved = notChecked.length > 0 ? await readIfExists(path) : void 0;
|
|
388
|
+
if (saved !== void 0) {
|
|
389
|
+
try {
|
|
390
|
+
const previous = parseLock(saved);
|
|
391
|
+
for (const name of notChecked) if (previous.servers[name] && !lock.servers[name]) lock.servers[name] = previous.servers[name];
|
|
392
|
+
lock.servers = Object.fromEntries(Object.entries(lock.servers).sort(([a], [b]) => a.localeCompare(b)));
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
await writeFile(path, `${JSON.stringify(lock, null, 2)}
|
|
397
|
+
`);
|
|
398
|
+
io.stderr(`wrote ${LOCK_FILE_PATH}
|
|
399
|
+
`);
|
|
400
|
+
}
|
|
401
|
+
return { source, warnings, incomplete };
|
|
402
|
+
}
|
|
403
|
+
function describeProjectConfig(config) {
|
|
404
|
+
const off = [
|
|
405
|
+
config.ignore.length > 0 ? `rules ${config.ignore.join(", ")}` : "",
|
|
406
|
+
config.ignorePaths.length > 0 ? `paths ${config.ignorePaths.join(", ")}` : "",
|
|
407
|
+
config.deps.minAgeDays < DEFAULT_CONFIG.deps.minAgeDays ? `DEPS-004 lowered to ${config.deps.minAgeDays} days` : ""
|
|
408
|
+
].filter(Boolean);
|
|
409
|
+
return off.length > 0 ? `${CONFIG_FILE_NAME} in the scanned project turns off checks: ${off.join("; ")} (--no-config ignores it)` : void 0;
|
|
410
|
+
}
|
|
411
|
+
async function runCommand(name, pathArg, opts, io) {
|
|
412
|
+
const root = await resolveRoot(io.cwd, pathArg);
|
|
413
|
+
let config;
|
|
414
|
+
let configNote;
|
|
415
|
+
if (opts.config === false) {
|
|
416
|
+
config = validateConfig({});
|
|
417
|
+
} else if (opts.config) {
|
|
418
|
+
config = await loadConfig(readIfExists, resolve(io.cwd, opts.config), true);
|
|
419
|
+
} else {
|
|
420
|
+
config = await loadConfig(readIfExists, join2(root, CONFIG_FILE_NAME), false);
|
|
421
|
+
configNote = describeProjectConfig(config);
|
|
422
|
+
}
|
|
423
|
+
const provider = createNodeProvider(root);
|
|
424
|
+
const tools = name === "mcp" ? await loadToolSource(root, provider, config, opts, io) : { warnings: [], incomplete: false };
|
|
425
|
+
const result = await runScanners(COMMANDS[name].scanners, {
|
|
426
|
+
provider,
|
|
427
|
+
config,
|
|
428
|
+
options: { offline: opts.offline === true, toolSource: tools.source, ...io.fetch ? { fetch: io.fetch } : {} }
|
|
429
|
+
});
|
|
430
|
+
const printed = configNote ? [configNote, ...result.warnings] : result.warnings;
|
|
431
|
+
for (const w of printed) io.stderr(`warning: ${safeText(w)}
|
|
432
|
+
`);
|
|
433
|
+
const incomplete = result.incomplete || tools.incomplete;
|
|
434
|
+
if (opts.format === "json") {
|
|
435
|
+
io.stdout(formatJson(result.findings, { warnings: [...tools.warnings, ...printed], incomplete }));
|
|
436
|
+
} else {
|
|
437
|
+
const color = opts.color && io.isTTY && !io.env.NO_COLOR;
|
|
438
|
+
io.stdout(formatText(result.findings, { color, incomplete }));
|
|
439
|
+
}
|
|
440
|
+
if (hasFailures(result.findings, opts.failOn)) return EXIT_FINDINGS;
|
|
441
|
+
if (opts.strict && incomplete) {
|
|
442
|
+
io.stderr("surfscan: --strict: some checks could not run, so the result is incomplete\n");
|
|
443
|
+
return EXIT_ERROR;
|
|
444
|
+
}
|
|
445
|
+
return EXIT_OK;
|
|
446
|
+
}
|
|
447
|
+
function buildProgram(io, setExit) {
|
|
448
|
+
const program = new Command();
|
|
449
|
+
program.name("surfscan").description(package_default.description).version(package_default.version).exitOverride().configureOutput({ writeOut: io.stdout, writeErr: io.stderr }).showHelpAfterError(false);
|
|
450
|
+
for (const [name, { summary, network }] of Object.entries(COMMANDS)) {
|
|
451
|
+
const command = program.command(name).description(summary).argument("[path]", "project directory to scan", ".").addOption(new Option("--format <format>", "output format").choices(["text", "json"]).default("text")).addOption(new Option("--fail-on <severity>", "exit 1 at or above this severity").choices([...SEVERITIES]).default("high")).option("--config <file>", `config file (default: ${CONFIG_FILE_NAME} in the scanned directory, if present)`).option("--no-config", `ignore any ${CONFIG_FILE_NAME} in the scanned directory; use when scanning untrusted changes`).option("--no-color", "disable colored output").option("--strict", "exit 2 if any check could not run (for example the registry was unreachable)").action(async (pathArg, opts) => {
|
|
452
|
+
setExit(await runCommand(name, pathArg, opts, io));
|
|
453
|
+
});
|
|
454
|
+
if (network) command.option("--offline", "do not contact the npm registry; run local checks only");
|
|
455
|
+
if (name === "mcp") {
|
|
456
|
+
command.option("--tools <file>", "exported tool list to check (MCP-001, MCP-002)").option("--connect", "launch each configured server and list its tools; runs third-party code").option("--yes", "skip the --connect confirmation (unpinned servers are still skipped)").option("--allow-unpinned", "with --connect --yes, also launch servers that are not pinned to a version").option("--update-lock", `write tool description hashes to ${LOCK_FILE_PATH}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return program;
|
|
460
|
+
}
|
|
461
|
+
function ignoreClosedPipe(stream) {
|
|
462
|
+
stream.on("error", (err) => {
|
|
463
|
+
if (err.code !== "EPIPE") throw err;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
async function main(argv, io) {
|
|
467
|
+
let exitCode = EXIT_OK;
|
|
468
|
+
const program = buildProgram(io, (code2) => {
|
|
469
|
+
exitCode = code2;
|
|
470
|
+
});
|
|
471
|
+
try {
|
|
472
|
+
if (argv.length === 0) {
|
|
473
|
+
program.outputHelp();
|
|
474
|
+
return EXIT_ERROR;
|
|
475
|
+
}
|
|
476
|
+
await program.parseAsync(argv, { from: "user" });
|
|
477
|
+
return exitCode;
|
|
478
|
+
} catch (err) {
|
|
479
|
+
if (err instanceof CommanderError) {
|
|
480
|
+
return err.code === "commander.helpDisplayed" || err.code === "commander.version" ? EXIT_OK : EXIT_ERROR;
|
|
481
|
+
}
|
|
482
|
+
if (err instanceof UsageError || err instanceof ConfigError) {
|
|
483
|
+
io.stderr(`surfscan: ${err.message}
|
|
484
|
+
`);
|
|
485
|
+
return EXIT_ERROR;
|
|
486
|
+
}
|
|
487
|
+
io.stderr(`surfscan: unexpected error: ${err.message}
|
|
488
|
+
`);
|
|
489
|
+
return EXIT_ERROR;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/cli/bin.ts
|
|
494
|
+
ignoreClosedPipe(process.stdout);
|
|
495
|
+
ignoreClosedPipe(process.stderr);
|
|
496
|
+
async function confirm(question, opts) {
|
|
497
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
498
|
+
try {
|
|
499
|
+
const answer = (await rl.question(question)).trim();
|
|
500
|
+
return opts?.exact !== void 0 ? answer === opts.exact : /^y(?:es)?$/i.test(answer);
|
|
501
|
+
} finally {
|
|
502
|
+
rl.close();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
var code = await main(process.argv.slice(2), {
|
|
506
|
+
stdout: (text) => process.stdout.write(text),
|
|
507
|
+
stderr: (text) => process.stderr.write(text),
|
|
508
|
+
cwd: process.cwd(),
|
|
509
|
+
env: process.env,
|
|
510
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
511
|
+
...process.stdin.isTTY ? { confirm } : {}
|
|
512
|
+
});
|
|
513
|
+
process.exitCode = code;
|
|
514
|
+
//# sourceMappingURL=bin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/bin.ts","../../src/cli/index.ts","../../src/cli/connect.ts","../../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { createInterface } from 'node:readline/promises';\nimport { ignoreClosedPipe, main } from './index.js';\n\nignoreClosedPipe(process.stdout);\nignoreClosedPipe(process.stderr);\n\nasync function confirm(question: string, opts?: { exact?: string }): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stderr });\n try {\n const answer = (await rl.question(question)).trim();\n return opts?.exact !== undefined ? answer === opts.exact : /^y(?:es)?$/i.test(answer);\n } finally {\n rl.close();\n }\n}\n\nconst code = await main(process.argv.slice(2), {\n stdout: (text) => process.stdout.write(text),\n stderr: (text) => process.stderr.write(text),\n cwd: process.cwd(),\n env: process.env,\n isTTY: Boolean(process.stdout.isTTY),\n ...(process.stdin.isTTY ? { confirm } : {}),\n});\nprocess.exitCode = code;\n","/**\n * CLI layer: argument parsing, printing and exit codes only. Scanning logic\n * lives in core/ and scanners/.\n */\nimport { mkdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { Command, CommanderError, Option } from 'commander';\nimport { CONFIG_FILE_NAME, ConfigError, DEFAULT_CONFIG, loadConfig, validateConfig } from '../core/config.js';\nimport { createNodeProvider } from '../core/node-provider.js';\nimport { formatJson, formatText } from '../core/report/index.js';\nimport { escapeUnsafe } from '../core/report/sanitize.js';\nimport { hasFailures, runScanners } from '../core/runner.js';\nimport { redactSecrets } from '../core/secret-patterns.js';\nimport { SEVERITIES } from '../core/types.js';\nimport type { Scanner, Severity, SourceProvider, SurfscanConfig } from '../core/types.js';\nimport { approvalsScanner } from '../scanners/approvals/index.js';\nimport { depsScanner } from '../scanners/deps/index.js';\nimport type { FetchLike } from '../scanners/deps/registry.js';\nimport { keysScanner } from '../scanners/keys/index.js';\nimport { parseMcpConfig, type McpServer } from '../scanners/mcp/config.js';\nimport { findMcpTargets, mcpScanner, unpinnedTarget } from '../scanners/mcp/index.js';\nimport { buildLock, LOCK_FILE_PATH, parseLock, parseToolList, type ToolSource } from '../scanners/mcp/tools.js';\nimport { connectToServers } from './connect.js';\nimport { rulesScanner } from '../scanners/rules/index.js';\nimport pkg from '../../package.json' with { type: 'json' };\n\nexport const EXIT_OK = 0;\nexport const EXIT_FINDINGS = 1;\nexport const EXIT_ERROR = 2;\n\nexport interface CliIO {\n stdout(text: string): void;\n stderr(text: string): void;\n cwd: string;\n env: Record<string, string | undefined>;\n isTTY: boolean;\n /** Ask the user a yes/no question. Absent when nobody can answer. */\n confirm?(question: string, opts?: { exact?: string }): Promise<boolean>;\n /** Replaces global fetch for registry lookups. Used by tests so none touch the network. */\n fetch?: FetchLike;\n}\n\ninterface CommonOptions {\n format: 'text' | 'json';\n failOn: Severity;\n /** False with --no-config. */\n config?: string | false;\n color: boolean;\n strict?: boolean;\n offline?: boolean;\n tools?: string;\n connect?: boolean;\n yes?: boolean;\n allowUnpinned?: boolean;\n updateLock?: boolean;\n}\n\nconst ALL_SCANNERS = [rulesScanner, mcpScanner, depsScanner, keysScanner, approvalsScanner];\n\nconst COMMANDS: Record<string, { scanners: Scanner[]; summary: string; network?: boolean }> = {\n rules: { scanners: [rulesScanner], summary: 'check agent instruction files for hidden or risky instructions' },\n mcp: { scanners: [mcpScanner], summary: 'audit MCP server configs and tool descriptions' },\n deps: { scanners: [depsScanner], summary: 'verify dependencies against the npm registry', network: true },\n keys: { scanners: [keysScanner], summary: 'find keys and secrets an agent can reach' },\n approvals: { scanners: [approvalsScanner], summary: 'find commands agents may run without asking' },\n scan: { scanners: ALL_SCANNERS, summary: 'run all scanners and merge the results', network: true },\n};\n\nclass UsageError extends Error {}\n\nasync function readIfExists(path: string): Promise<string | undefined> {\n try {\n return await readFile(path, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw err;\n }\n}\n\nasync function resolveRoot(cwd: string, pathArg: string | undefined): Promise<string> {\n const root = resolve(cwd, pathArg ?? '.');\n let isDir = false;\n try {\n isDir = (await stat(root)).isDirectory();\n } catch {\n throw new UsageError(`path not found: ${pathArg ?? '.'}`);\n }\n if (!isDir) throw new UsageError(`not a directory: ${pathArg}`);\n return root;\n}\n\nconst safeText = (text: string): string => escapeUnsafe(redactSecrets(text));\n\nasync function configuredServers(provider: SourceProvider, config: SurfscanConfig, io: CliIO): Promise<McpServer[]> {\n const servers: McpServer[] = [];\n for (const path of await findMcpTargets(provider, config)) {\n const content = await provider.read(path);\n if (content === undefined) continue;\n try {\n servers.push(...parseMcpConfig({ path, content }));\n } catch (err) {\n io.stderr(`warning: ${safeText(`${path}: could not parse MCP config (${(err as Error).message})`)}\\n`);\n }\n }\n return servers;\n}\n\n/** Env names a server will receive from its config, marking the ones copied from your shell. Never values. */\nfunction describeEnv(server: McpServer): string {\n const names = Object.entries(server.env).map(([key, value]) => {\n const fromShell = [...value.matchAll(/\\$\\{(?:env:)?([A-Za-z_]\\w*)\\}/g)].map((m) => `$${m[1]}`);\n return fromShell.length > 0 ? `${key} (from your shell: ${fromShell.join(', ')})` : key;\n });\n return names.length > 0 ? names.join(', ') : 'none';\n}\n\nasync function connectSource(\n provider: SourceProvider,\n config: SurfscanConfig,\n opts: CommonOptions,\n io: CliIO,\n): Promise<{ source: ToolSource; notChecked: string[]; warnings: string[]; incomplete: boolean }> {\n const servers = await configuredServers(provider, config, io);\n const warnings: string[] = [];\n const warn = (message: string): void => {\n warnings.push(message);\n io.stderr(`warning: ${safeText(message)}\\n`);\n };\n let skippedUnpinned = false;\n io.stderr('WARNING: --connect runs third-party code on this machine.\\n');\n io.stderr('Each server gets a minimal environment (PATH, HOME, temp folders, proxy and certificate settings)\\n');\n io.stderr('plus the variables listed under env. Your other shell variables are not passed on.\\n\\n');\n\n const approved: McpServer[] = [];\n for (const s of servers) {\n if (!s.command) {\n warn(`${s.name}: skipped, --connect only launches local servers, not remote URLs`);\n continue;\n }\n const target = unpinnedTarget(s);\n io.stderr(`${safeText(s.name)}\\n runs: ${safeText([s.command, ...s.args].join(' '))}\\n env: ${safeText(describeEnv(s))}\\n`);\n if (target) io.stderr(` NOT PINNED: it runs whatever is newest (${safeText(target)}), which may differ from what you reviewed\\n`);\n if (/\\$\\{(?:env:)?[A-Za-z_]\\w*\\}/.test([s.command, ...s.args].join(' '))) {\n io.stderr(' note: ${VAR} is only expanded in env, so here it is passed as written\\n');\n }\n\n if (opts.yes) {\n if (target && !opts.allowUnpinned) {\n warn(`${s.name}: skipped because it is not pinned (pin a version, or pass --allow-unpinned)`);\n skippedUnpinned = true;\n } else {\n approved.push(s);\n }\n continue;\n }\n if (!io.confirm) throw new UsageError('--connect needs confirmation; nothing was launched (pass --yes to skip the question)');\n const ok = target\n ? await io.confirm(' Not pinned. Type \"yes\" to launch it anyway: ', { exact: 'yes' })\n : await io.confirm(' Launch this server? [y/N] ');\n if (ok) approved.push(s);\n else io.stderr(' skipped\\n');\n }\n if (approved.length === 0) throw new UsageError('--connect: no servers were launched');\n\n const tools = await connectToServers(approved, io.env, warn);\n // Declining a server at the prompt is a choice. A failure, or a skip nobody was there to decide, is not.\n const incomplete = skippedUnpinned || approved.some((s) => !(s.name in tools));\n return { source: { tools }, notChecked: servers.map((s) => s.name).filter((name) => !(name in tools)), warnings, incomplete };\n}\n\ninterface LoadedTools {\n source?: ToolSource;\n warnings: string[];\n incomplete: boolean;\n}\n\nasync function loadToolSource(root: string, provider: SourceProvider, config: SurfscanConfig, opts: CommonOptions, io: CliIO): Promise<LoadedTools> {\n if (opts.tools && opts.connect) throw new UsageError('use either --tools or --connect, not both');\n if (opts.updateLock && !opts.tools && !opts.connect) {\n throw new UsageError('--update-lock needs a tool list: pass --tools <file.json> or --connect');\n }\n\n let source: ToolSource | undefined;\n let notChecked: string[] = [];\n let warnings: string[] = [];\n let incomplete = false;\n if (opts.connect) {\n ({ source, notChecked, warnings, incomplete } = await connectSource(provider, config, opts, io));\n } else if (opts.tools) {\n const content = await readIfExists(resolve(io.cwd, opts.tools));\n if (content === undefined) throw new UsageError(`tool list not found: ${opts.tools}`);\n try {\n source = { file: opts.tools.replace(/\\\\/g, '/'), content, tools: parseToolList(content) };\n } catch (err) {\n throw new UsageError(`${opts.tools}: invalid tool list (${(err as Error).message})`);\n }\n }\n\n if (source && opts.updateLock) {\n const path = join(root, LOCK_FILE_PATH);\n await mkdir(dirname(path), { recursive: true });\n const lock = await buildLock(source.tools);\n // Servers that were skipped or failed to start keep their saved hashes, so a partial run cannot erase them.\n const saved = notChecked.length > 0 ? await readIfExists(path) : undefined;\n if (saved !== undefined) {\n try {\n const previous = parseLock(saved);\n for (const name of notChecked) if (previous.servers[name] && !lock.servers[name]) lock.servers[name] = previous.servers[name]!;\n lock.servers = Object.fromEntries(Object.entries(lock.servers).sort(([a], [b]) => a.localeCompare(b)));\n } catch {\n // An unreadable old lock is replaced, as before.\n }\n }\n await writeFile(path, `${JSON.stringify(lock, null, 2)}\\n`);\n io.stderr(`wrote ${LOCK_FILE_PATH}\\n`);\n }\n return { source, warnings, incomplete };\n}\n\n/** The checks a config in the scanned project turns off. That file can change in the same pull request being scanned. */\nfunction describeProjectConfig(config: SurfscanConfig): string | undefined {\n const off = [\n config.ignore.length > 0 ? `rules ${config.ignore.join(', ')}` : '',\n config.ignorePaths.length > 0 ? `paths ${config.ignorePaths.join(', ')}` : '',\n config.deps.minAgeDays < DEFAULT_CONFIG.deps.minAgeDays ? `DEPS-004 lowered to ${config.deps.minAgeDays} days` : '',\n ].filter(Boolean);\n return off.length > 0 ? `${CONFIG_FILE_NAME} in the scanned project turns off checks: ${off.join('; ')} (--no-config ignores it)` : undefined;\n}\n\nasync function runCommand(name: string, pathArg: string | undefined, opts: CommonOptions, io: CliIO): Promise<number> {\n const root = await resolveRoot(io.cwd, pathArg);\n let config: SurfscanConfig;\n let configNote: string | undefined;\n if (opts.config === false) {\n config = validateConfig({});\n } else if (opts.config) {\n config = await loadConfig(readIfExists, resolve(io.cwd, opts.config), true);\n } else {\n config = await loadConfig(readIfExists, join(root, CONFIG_FILE_NAME), false);\n configNote = describeProjectConfig(config);\n }\n\n const provider = createNodeProvider(root);\n const tools: LoadedTools = name === 'mcp' ? await loadToolSource(root, provider, config, opts, io) : { warnings: [], incomplete: false };\n\n const result = await runScanners(COMMANDS[name]!.scanners, {\n provider,\n config,\n options: { offline: opts.offline === true, toolSource: tools.source, ...(io.fetch ? { fetch: io.fetch } : {}) },\n });\n\n const printed = configNote ? [configNote, ...result.warnings] : result.warnings;\n for (const w of printed) io.stderr(`warning: ${safeText(w)}\\n`);\n const incomplete = result.incomplete || tools.incomplete;\n\n if (opts.format === 'json') {\n // --connect warnings were printed as they happened, but the report must still explain them.\n io.stdout(formatJson(result.findings, { warnings: [...tools.warnings, ...printed], incomplete }));\n } else {\n const color = opts.color && io.isTTY && !io.env.NO_COLOR;\n io.stdout(formatText(result.findings, { color, incomplete }));\n }\n\n // Findings win: exit 1 already fails the build, and says more than 2 would.\n if (hasFailures(result.findings, opts.failOn)) return EXIT_FINDINGS;\n if (opts.strict && incomplete) {\n io.stderr('surfscan: --strict: some checks could not run, so the result is incomplete\\n');\n return EXIT_ERROR;\n }\n return EXIT_OK;\n}\n\nfunction buildProgram(io: CliIO, setExit: (code: number) => void): Command {\n const program = new Command();\n program\n .name('surfscan')\n .description(pkg.description)\n .version(pkg.version)\n .exitOverride()\n .configureOutput({ writeOut: io.stdout, writeErr: io.stderr })\n .showHelpAfterError(false);\n\n for (const [name, { summary, network }] of Object.entries(COMMANDS)) {\n const command = program\n .command(name)\n .description(summary)\n .argument('[path]', 'project directory to scan', '.')\n .addOption(new Option('--format <format>', 'output format').choices(['text', 'json']).default('text'))\n .addOption(new Option('--fail-on <severity>', 'exit 1 at or above this severity').choices([...SEVERITIES]).default('high'))\n .option('--config <file>', `config file (default: ${CONFIG_FILE_NAME} in the scanned directory, if present)`)\n .option('--no-config', `ignore any ${CONFIG_FILE_NAME} in the scanned directory; use when scanning untrusted changes`)\n .option('--no-color', 'disable colored output')\n .option('--strict', 'exit 2 if any check could not run (for example the registry was unreachable)')\n .action(async (pathArg: string, opts: CommonOptions) => {\n setExit(await runCommand(name, pathArg, opts, io));\n });\n if (network) command.option('--offline', 'do not contact the npm registry; run local checks only');\n if (name === 'mcp') {\n command\n .option('--tools <file>', 'exported tool list to check (MCP-001, MCP-002)')\n .option('--connect', 'launch each configured server and list its tools; runs third-party code')\n .option('--yes', 'skip the --connect confirmation (unpinned servers are still skipped)')\n .option('--allow-unpinned', 'with --connect --yes, also launch servers that are not pinned to a version')\n .option('--update-lock', `write tool description hashes to ${LOCK_FILE_PATH}`);\n }\n }\n return program;\n}\n\n/**\n * Keep running when the reader goes away, as in `surfscan scan . | head`. The\n * exit code still reflects the scan, so a closed pipe cannot hide a finding.\n */\nexport function ignoreClosedPipe(stream: Pick<NodeJS.WriteStream, 'on'>): void {\n stream.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code !== 'EPIPE') throw err;\n });\n}\n\n/** Run the CLI. Returns the process exit code; never calls process.exit. */\nexport async function main(argv: string[], io: CliIO): Promise<number> {\n let exitCode = EXIT_OK;\n const program = buildProgram(io, (code) => {\n exitCode = code;\n });\n\n try {\n if (argv.length === 0) {\n program.outputHelp();\n return EXIT_ERROR;\n }\n await program.parseAsync(argv, { from: 'user' });\n return exitCode;\n } catch (err) {\n if (err instanceof CommanderError) {\n return err.code === 'commander.helpDisplayed' || err.code === 'commander.version' ? EXIT_OK : EXIT_ERROR;\n }\n if (err instanceof UsageError || err instanceof ConfigError) {\n io.stderr(`surfscan: ${err.message}\\n`);\n return EXIT_ERROR;\n }\n io.stderr(`surfscan: unexpected error: ${(err as Error).message}\\n`);\n return EXIT_ERROR;\n }\n}\n","/**\n * `mcp --connect`: launches configured MCP servers and lists their tools. This\n * runs third-party code, so it lives in the CLI layer and only runs after the\n * user confirms.\n */\nimport { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process';\nimport { join } from 'node:path';\nimport type { McpServer } from '../scanners/mcp/config.js';\nimport { parseToolList, UNNAMED_SERVER, type McpTool, type ToolList } from '../scanners/mcp/tools.js';\n\nconst TIMEOUT_MS = 30_000;\nconst MAX_OUTPUT_CHARS = 16 * 1024 * 1024;\nconst PROTOCOL_VERSION = '2025-06-18';\n\nfunction expand(value: string, env: NodeJS.ProcessEnv): string {\n return value.replace(/\\$\\{(?:env:)?([A-Za-z_]\\w*)\\}/g, (whole, name: string) => env[name] ?? whole);\n}\n\nfunction winQuote(arg: string): string {\n return /[\\s\"&|<>^]/.test(arg) ? `\"${arg.replace(/\"/g, '\"\"')}\"` : arg;\n}\n\n/**\n * Variables a server needs just to start (find node, npx, uvx, docker; reach\n * the network through a proxy). Compared case-insensitively, since Windows\n * variable names are. Everything else in the shell, such as GITHUB_TOKEN or\n * AWS_SECRET_ACCESS_KEY, stays out unless the server's own config asks for it.\n */\nconst BASE_ENV_ALLOWLIST = new Set(\n [\n 'PATH', 'PATHEXT', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',\n 'TEMP', 'TMP', 'TMPDIR', 'SystemRoot', 'SystemDrive', 'WINDIR', 'ComSpec', 'ProgramFiles',\n 'ProgramFiles(x86)', 'ProgramData', 'USER', 'USERNAME', 'LOGNAME', 'SHELL', 'TERM', 'LANG',\n 'LC_ALL', 'LC_CTYPE', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'HTTP_PROXY',\n 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY', 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',\n ].map((name) => name.toLowerCase()),\n);\n\n/** The environment a launched server gets: the allowlist, plus the `env` block from its config. */\nexport function buildChildEnv(server: McpServer, shellEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {};\n for (const [key, value] of Object.entries(shellEnv)) {\n if (value !== undefined && BASE_ENV_ALLOWLIST.has(key.toLowerCase())) env[key] = value;\n }\n // ${VAR} in the env block is the one way for a shell variable to reach a server. The prompt shows\n // those names. Commands and arguments are not expanded: anyone can read a command line.\n for (const [key, value] of Object.entries(server.env)) env[key] = expand(value, shellEnv);\n return env;\n}\n\n/**\n * Stop a server and release its pipes, so surfscan can exit even if something\n * the server started keeps running. On Windows the server is a child of the\n * shell, so the whole tree has to go. taskkill is named by full path because\n * Windows searches the current folder, which here is a project being scanned.\n */\nfunction stopServer(child: ChildProcessWithoutNullStreams): void {\n child.stdin.destroy();\n const systemRoot = process.env.SystemRoot;\n if (process.platform === 'win32' && child.pid !== undefined && systemRoot) {\n spawnSync(join(systemRoot, 'System32', 'taskkill.exe'), ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });\n }\n child.kill();\n child.stdout.destroy();\n child.unref();\n}\n\n/** Launch one server over stdio, list its tools, then stop it. */\nexport function listServerTools(server: McpServer, baseEnv: NodeJS.ProcessEnv): Promise<McpTool[]> {\n return new Promise((resolve, reject) => {\n const env = buildChildEnv(server, baseEnv);\n // Not expanded, so that a shell variable cannot reach a command line. See buildChildEnv.\n const command = server.command!;\n const args = server.args;\n\n // npx and similar are .cmd files on Windows, which only a shell can start.\n const windows = process.platform === 'win32';\n // cmd.exe expands %VAR% even inside quotes, so what ran would differ from what the user approved.\n if (windows && [command, ...args].some((part) => /%[^%\\s]+%/.test(part))) {\n reject(new Error('%VAR% in a command or argument cannot be passed safely on Windows'));\n return;\n }\n const child = windows\n ? spawn([command, ...args].map(winQuote).join(' '), { env, shell: true, stdio: ['pipe', 'pipe', 'ignore'], windowsHide: true })\n : spawn(command, args, { env, stdio: ['pipe', 'pipe', 'ignore'] });\n\n const tools: unknown[] = [];\n let buffer = '';\n let received = 0;\n let finished = false;\n const finish = (err: Error | undefined): void => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n stopServer(child as unknown as ChildProcessWithoutNullStreams);\n if (err) reject(err);\n else resolve(parseToolList(JSON.stringify({ tools }))[UNNAMED_SERVER]!);\n };\n const timer = setTimeout(() => finish(new Error(`no tool list after ${TIMEOUT_MS / 1000} seconds`)), TIMEOUT_MS);\n const send = (message: object): void => {\n child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\\n`);\n };\n\n child.on('error', (err) => finish(err));\n child.on('exit', (code) => finish(new Error(`server exited early with code ${code}`)));\n child.stdin.on('error', () => undefined);\n child.stdout.setEncoding('utf8');\n child.stdout.on('data', (chunk: string) => {\n received += chunk.length;\n if (received > MAX_OUTPUT_CHARS) return finish(new Error('server sent too much output'));\n buffer += chunk;\n let newline: number;\n while ((newline = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newline).trim();\n buffer = buffer.slice(newline + 1);\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n // Valid JSON is not always a message: a server can print null or 42.\n if (typeof parsed !== 'object' || parsed === null) continue;\n const message = parsed as { id?: unknown; error?: { message?: unknown }; result?: { tools?: unknown; nextCursor?: unknown } };\n if (message.error) return finish(new Error(String(message.error.message)));\n if (message.id === 1) {\n send({ method: 'notifications/initialized' });\n send({ id: 2, method: 'tools/list', params: {} });\n } else if (message.id === 2) {\n // Not push(...tools): a long enough page would overflow the call stack.\n if (Array.isArray(message.result?.tools)) for (const tool of message.result.tools) tools.push(tool);\n const cursor = message.result?.nextCursor;\n if (typeof cursor === 'string' && cursor) send({ id: 2, method: 'tools/list', params: { cursor } });\n else return finish(undefined);\n }\n }\n });\n\n send({\n id: 1,\n method: 'initialize',\n params: { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'surfscan', version: '0' } },\n });\n });\n}\n\n/** Tool lists for every stdio server. A server that fails is reported and left out. */\nexport async function connectToServers(\n servers: McpServer[],\n env: NodeJS.ProcessEnv,\n warn: (message: string) => void,\n): Promise<ToolList> {\n const tools: ToolList = {};\n for (const server of servers) {\n if (!server.command) {\n warn(`${server.name}: skipped, --connect only launches local servers, not remote URLs`);\n continue;\n }\n try {\n tools[server.name] = await listServerTools(server, env);\n } catch (err) {\n warn(`${server.name}: could not list tools (${(err as Error).message})`);\n }\n }\n return tools;\n}\n","{\n \"name\": \"surfscan\",\n \"version\": \"0.1.1\",\n \"description\": \"Attack surface scanner for AI-assisted development\",\n \"keywords\": [\n \"ai\",\n \"security\",\n \"mcp\",\n \"rag\",\n \"prompt-injection\",\n \"llm\",\n \"scanner\"\n ],\n \"license\": \"MIT\",\n \"author\": \"Omogbai Atakpu\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/githero20/surfscan.git\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/githero20/surfscan/issues\"\n },\n \"homepage\": \"https://github.com/githero20/surfscan#readme\",\n \"type\": \"module\",\n \"bin\": {\n \"surfscan\": \"dist/cli/bin.js\"\n },\n \"main\": \"dist/index.js\",\n \"types\": \"dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\",\n \"CHANGELOG.md\",\n \"docs\"\n ],\n \"engines\": {\n \"node\": \">=20\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"prepublishOnly\": \"npm run typecheck && npm test && npm run build\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\"\n },\n \"dependencies\": {\n \"commander\": \"^14.0.3\",\n \"fast-glob\": \"^3.3.3\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^22.20.5\",\n \"tsup\": \"^9.0.0\",\n \"typescript\": \"^5.9.3\",\n \"vitest\": \"^5.1.0\"\n },\n \"allowScripts\": {\n \"esbuild@0.28.2\": true\n }\n}\n"],"mappings":"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","names":["join","resolve","code","join","code"]}
|