agent-security-scanner-mcp 4.3.0 → 4.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.
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * npm-download.js
5
+ *
6
+ * Safe npm package downloading WITHOUT code execution
7
+ *
8
+ * Security Features:
9
+ * - Uses npm pack instead of npm install (no lifecycle scripts)
10
+ * - Extracts tarball manually without running any code
11
+ * - Size limits to prevent tarball bombs
12
+ * - Path traversal protection
13
+ * - Timeout protection
14
+ */
15
+
16
+ import { exec } from 'child_process';
17
+ import { promisify } from 'util';
18
+ import fs from 'fs/promises';
19
+ import { createReadStream, existsSync } from 'fs';
20
+ import path from 'path';
21
+ import * as tar from 'tar';
22
+
23
+ const execAsync = promisify(exec);
24
+
25
+ const MAX_PACKAGE_SIZE_MB = 200; // 200MB limit
26
+ const PACK_TIMEOUT_MS = 60000; // 1 minute
27
+ const MAX_EXTRACTED_SIZE_MB = 500; // 500MB limit after extraction
28
+
29
+ /**
30
+ * Download npm package tarball using npm pack (no scripts executed)
31
+ * @param {string} packageName - npm package name with optional version
32
+ * @param {string} destDir - Destination directory
33
+ * @param {object} options - Options
34
+ * @returns {Promise<{success: boolean, tarballPath?: string, error?: string}>}
35
+ */
36
+ export async function downloadNpmPackage(packageName, destDir, options = {}) {
37
+ const {
38
+ timeout = PACK_TIMEOUT_MS,
39
+ } = options;
40
+
41
+ try {
42
+ // Create destination directory
43
+ await fs.mkdir(destDir, { recursive: true });
44
+
45
+ // npm pack downloads the tarball WITHOUT running any scripts
46
+ const packCmd = `cd "${destDir}" && npm pack ${packageName} --dry-run=false 2>&1`;
47
+
48
+ const { stdout, stderr } = await execAsync(packCmd, {
49
+ timeout,
50
+ maxBuffer: 10 * 1024 * 1024,
51
+ });
52
+
53
+ // Find the downloaded tarball (npm pack outputs the filename)
54
+ const lines = stdout.split('\n');
55
+ const tarballLine = lines.find(line => line.endsWith('.tgz'));
56
+
57
+ if (!tarballLine) {
58
+ return {
59
+ success: false,
60
+ error: 'npm pack did not produce a tarball',
61
+ };
62
+ }
63
+
64
+ const tarballPath = path.join(destDir, tarballLine.trim());
65
+
66
+ // Check tarball size
67
+ const stats = await fs.stat(tarballPath);
68
+ const sizeMB = stats.size / (1024 * 1024);
69
+
70
+ if (sizeMB > MAX_PACKAGE_SIZE_MB) {
71
+ await fs.unlink(tarballPath);
72
+ return {
73
+ success: false,
74
+ error: `Package too large: ${sizeMB.toFixed(1)}MB > ${MAX_PACKAGE_SIZE_MB}MB limit`,
75
+ };
76
+ }
77
+
78
+ return {
79
+ success: true,
80
+ tarballPath,
81
+ size: sizeMB,
82
+ };
83
+
84
+ } catch (error) {
85
+ return {
86
+ success: false,
87
+ error: error.message,
88
+ };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Extract npm tarball with security checks
94
+ * @param {string} tarballPath - Path to tarball
95
+ * @param {string} extractDir - Directory to extract to
96
+ * @returns {Promise<{success: boolean, path?: string, error?: string}>}
97
+ */
98
+ export async function extractNpmTarball(tarballPath, extractDir) {
99
+ try {
100
+ // Create extraction directory
101
+ await fs.mkdir(extractDir, { recursive: true });
102
+
103
+ // Extract tarball with security options
104
+ await tar.x({
105
+ file: tarballPath,
106
+ cwd: extractDir,
107
+ strip: 1, // Remove 'package/' prefix from npm tarballs
108
+
109
+ // Security options
110
+ filter: (path, entry) => {
111
+ // Block path traversal attempts
112
+ if (path.includes('..')) {
113
+ console.warn(`Blocked path traversal attempt: ${path}`);
114
+ return false;
115
+ }
116
+
117
+ // Block absolute paths
118
+ if (path.startsWith('/')) {
119
+ console.warn(`Blocked absolute path: ${path}`);
120
+ return false;
121
+ }
122
+
123
+ // Block symlinks (potential security risk)
124
+ if (entry.type === 'SymbolicLink') {
125
+ console.warn(`Blocked symlink: ${path}`);
126
+ return false;
127
+ }
128
+
129
+ // Block files larger than 50MB individually
130
+ if (entry.size > 50 * 1024 * 1024) {
131
+ console.warn(`Blocked large file: ${path} (${entry.size} bytes)`);
132
+ return false;
133
+ }
134
+
135
+ return true;
136
+ },
137
+
138
+ // Prevent archive bombs
139
+ maxReadSize: MAX_EXTRACTED_SIZE_MB * 1024 * 1024,
140
+ });
141
+
142
+ // Check total extracted size
143
+ const sizeCmd = `du -sm "${extractDir}" | cut -f1`;
144
+ const { stdout } = await execAsync(sizeCmd);
145
+ const sizeMB = parseInt(stdout.trim(), 10);
146
+
147
+ if (sizeMB > MAX_EXTRACTED_SIZE_MB) {
148
+ // Delete oversized extraction
149
+ await fs.rm(extractDir, { recursive: true, force: true });
150
+ return {
151
+ success: false,
152
+ error: `Extracted size too large: ${sizeMB}MB > ${MAX_EXTRACTED_SIZE_MB}MB limit`,
153
+ };
154
+ }
155
+
156
+ return {
157
+ success: true,
158
+ path: extractDir,
159
+ size: sizeMB,
160
+ };
161
+
162
+ } catch (error) {
163
+ return {
164
+ success: false,
165
+ error: error.message,
166
+ };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Full workflow: download + extract + cleanup tarball
172
+ * @param {string} packageName - npm package name
173
+ * @param {string} workDir - Working directory
174
+ * @returns {Promise<{success: boolean, sourcePath?: string, error?: string}>}
175
+ */
176
+ export async function downloadAndExtract(packageName, workDir) {
177
+ const tempDownloadDir = path.join(workDir, 'temp-downloads');
178
+ const extractDir = path.join(workDir, 'extracted-source');
179
+
180
+ try {
181
+ // Step 1: Download tarball
182
+ const downloadResult = await downloadNpmPackage(packageName, tempDownloadDir);
183
+
184
+ if (!downloadResult.success) {
185
+ return downloadResult;
186
+ }
187
+
188
+ // Step 2: Extract tarball
189
+ const extractResult = await extractNpmTarball(downloadResult.tarballPath, extractDir);
190
+
191
+ // Step 3: Clean up tarball and download directory
192
+ await fs.rm(tempDownloadDir, { recursive: true, force: true });
193
+
194
+ if (!extractResult.success) {
195
+ // Clean up extracted files on failure
196
+ await fs.rm(extractDir, { recursive: true, force: true });
197
+ return extractResult;
198
+ }
199
+
200
+ return {
201
+ success: true,
202
+ sourcePath: extractDir,
203
+ packageSize: downloadResult.size,
204
+ extractedSize: extractResult.size,
205
+ };
206
+
207
+ } catch (error) {
208
+ // Clean up everything on error
209
+ await fs.rm(tempDownloadDir, { recursive: true, force: true }).catch(() => {});
210
+ await fs.rm(extractDir, { recursive: true, force: true }).catch(() => {});
211
+
212
+ return {
213
+ success: false,
214
+ error: error.message,
215
+ };
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Check if an npm package exists (without downloading)
221
+ * @param {string} packageName - Package name to check
222
+ * @returns {Promise<boolean>}
223
+ */
224
+ export async function npmPackageExists(packageName) {
225
+ try {
226
+ // Use npm view to check if package exists
227
+ const viewCmd = `npm view ${packageName} name 2>&1`;
228
+ const { stdout } = await execAsync(viewCmd, { timeout: 10000 });
229
+
230
+ return stdout.trim().length > 0;
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Get package.json metadata without downloading full package
238
+ * @param {string} packageName - Package name
239
+ * @returns {Promise<{success: boolean, metadata?: object, error?: string}>}
240
+ */
241
+ export async function getPackageMetadata(packageName) {
242
+ try {
243
+ const viewCmd = `npm view ${packageName} --json`;
244
+ const { stdout } = await execAsync(viewCmd, { timeout: 10000 });
245
+
246
+ const metadata = JSON.parse(stdout);
247
+
248
+ return {
249
+ success: true,
250
+ metadata: {
251
+ name: metadata.name,
252
+ version: metadata.version,
253
+ description: metadata.description,
254
+ homepage: metadata.homepage,
255
+ repository: metadata.repository,
256
+ dependencies: metadata.dependencies || {},
257
+ },
258
+ };
259
+ } catch (error) {
260
+ return {
261
+ success: false,
262
+ error: error.message,
263
+ };
264
+ }
265
+ }