muaddib-scanner 2.11.146 → 2.11.148
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/package.json
CHANGED
package/src/rules/index.js
CHANGED
|
@@ -1148,7 +1148,7 @@ const RULES = {
|
|
|
1148
1148
|
severity: 'CRITICAL',
|
|
1149
1149
|
confidence: 'high',
|
|
1150
1150
|
domain: 'malware',
|
|
1151
|
-
description: 'Reference dissimulee (charcode/base64/hex) OU enumeration de process.env cherchant un prefix marqueur MUADDIB (marker-agnostique), vers un marqueur d\'environnement d\'analyse — marqueur de sandbox MUAD\'DIB (MUADDIB_GVISOR), marqueur d\'un analyseur pair, ou nom de poison-token. Seules les formes obfusquees/enumerees sont matchees : le code de sandbox de MUAD\'DIB lui-meme et l\'outillage securite legitime referencent ces marqueurs en clair sans risque. Aucun package legitime n\'encode ni n\'enumere un check pour ces tripwires. Le marqueur est un honeytoken plante : garde public et stable pour que tout verificateur s\'auto-incrimine.',
|
|
1151
|
+
description: 'Reference dissimulee (charcode/base64/hex) OU enumeration de process.env (JS) / os.environ (Python) cherchant un prefix marqueur MUADDIB (marker-agnostique), vers un marqueur d\'environnement d\'analyse — marqueur de sandbox MUAD\'DIB (MUADDIB_GVISOR), marqueur d\'un analyseur pair, ou nom de poison-token. Seules les formes obfusquees/enumerees sont matchees : le code de sandbox de MUAD\'DIB lui-meme et l\'outillage securite legitime referencent ces marqueurs en clair sans risque. Aucun package legitime n\'encode ni n\'enumere un check pour ces tripwires. Le marqueur est un honeytoken plante : garde public et stable pour que tout verificateur s\'auto-incrimine.',
|
|
1152
1152
|
references: [
|
|
1153
1153
|
'https://attack.mitre.org/techniques/T1497/',
|
|
1154
1154
|
'https://attack.mitre.org/techniques/T1480/'
|
package/src/sandbox/index.js
CHANGED
|
@@ -195,13 +195,51 @@ function imageExists() {
|
|
|
195
195
|
|
|
196
196
|
// ── gVisor availability check ──
|
|
197
197
|
|
|
198
|
+
let _gvisorAvailableCache; // memoized host capability (runsc does not appear mid-process)
|
|
199
|
+
let _sandboxRuntimeWarned = false; // throttle the runtime warning/refusal log to once per process
|
|
200
|
+
|
|
198
201
|
function isGvisorAvailable() {
|
|
202
|
+
if (_gvisorAvailableCache !== undefined) return _gvisorAvailableCache;
|
|
199
203
|
try {
|
|
200
204
|
const info = execSync('docker info', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
|
|
201
|
-
|
|
205
|
+
_gvisorAvailableCache = /\brunsc\b/.test(info);
|
|
202
206
|
} catch {
|
|
203
|
-
|
|
207
|
+
_gvisorAvailableCache = false;
|
|
204
208
|
}
|
|
209
|
+
return _gvisorAvailableCache;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Pure runtime-selection decision (no I/O) — unit-testable without Docker, unlike
|
|
213
|
+
// runSandbox() which early-returns before this point when Docker is absent.
|
|
214
|
+
// Inputs: the env object + whether runsc is actually available (isGvisorAvailable()).
|
|
215
|
+
// Returns { useGvisor, refuse, warn, reason }:
|
|
216
|
+
// - useGvisor: run under runsc (stronger isolation)
|
|
217
|
+
// - refuse: caller MUST NOT run — gVisor was REQUIRED but runsc is unavailable
|
|
218
|
+
// (fail CLOSED, opt-in via MUADDIB_REQUIRE_GVISOR). Do not downgrade.
|
|
219
|
+
// - warn: caller should emit a prominent isolation-downgrade WARNING because
|
|
220
|
+
// gVisor was requested, is unavailable, but was not required (fail OPEN).
|
|
221
|
+
// Default (neither env var set) → runc, no warn, no refuse (unchanged behavior).
|
|
222
|
+
// MUADDIB_REQUIRE_GVISOR implies wanting gVisor (you cannot require what you don't want).
|
|
223
|
+
function resolveSandboxRuntime(env, gvisorAvailable) {
|
|
224
|
+
const wantGvisor = env.MUADDIB_SANDBOX_RUNTIME === 'gvisor';
|
|
225
|
+
const requireGvisor = env.MUADDIB_REQUIRE_GVISOR === '1' || env.MUADDIB_REQUIRE_GVISOR === 'true';
|
|
226
|
+
if (!wantGvisor && !requireGvisor) {
|
|
227
|
+
// gVisor not requested. If runsc IS available on this host, still warn: the operator
|
|
228
|
+
// installed gVisor but this scan is running on weaker runc (the exact VPS gap — runsc
|
|
229
|
+
// present, Docker default runtime is runc, env var never set). Detectable in prod.
|
|
230
|
+
if (gvisorAvailable) {
|
|
231
|
+
return { useGvisor: false, refuse: false, warn: true,
|
|
232
|
+
reason: 'sandbox running under runc although gVisor (runsc) IS available on this host — set MUADDIB_SANDBOX_RUNTIME=gvisor to use the stronger isolation' };
|
|
233
|
+
}
|
|
234
|
+
return { useGvisor: false, refuse: false, warn: false, reason: 'runc (default — gVisor neither requested nor available)' };
|
|
235
|
+
}
|
|
236
|
+
if (gvisorAvailable) {
|
|
237
|
+
return { useGvisor: true, refuse: false, warn: false, reason: 'gvisor (runsc)' };
|
|
238
|
+
}
|
|
239
|
+
if (requireGvisor) {
|
|
240
|
+
return { useGvisor: false, refuse: true, warn: false, reason: 'gVisor REQUIRED (MUADDIB_REQUIRE_GVISOR) but runsc is not available' };
|
|
241
|
+
}
|
|
242
|
+
return { useGvisor: false, refuse: false, warn: true, reason: 'gVisor requested but runsc unavailable — falling back to weaker runc isolation' };
|
|
205
243
|
}
|
|
206
244
|
|
|
207
245
|
// ── Build image (with cache) ──
|
|
@@ -725,15 +763,37 @@ async function runSandbox(packageName, options = {}) {
|
|
|
725
763
|
return cleanResult;
|
|
726
764
|
}
|
|
727
765
|
|
|
728
|
-
// Detect sandbox runtime (gVisor or default Docker/runc)
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
766
|
+
// Detect sandbox runtime (gVisor or default Docker/runc). The decision is a pure,
|
|
767
|
+
// unit-tested function (resolveSandboxRuntime); here we act on it. isGvisorAvailable()
|
|
768
|
+
// is memoized, so this is a per-process `docker info`, not a per-scan one.
|
|
769
|
+
const rt = resolveSandboxRuntime(process.env, isGvisorAvailable());
|
|
770
|
+
const useGvisor = rt.useGvisor;
|
|
771
|
+
if (rt.refuse) {
|
|
772
|
+
// Fail CLOSED: operator set MUADDIB_REQUIRE_GVISOR but runsc is unavailable. Do NOT
|
|
773
|
+
// silently downgrade to weaker runc isolation — return INCONCLUSIVE (score -1) so the
|
|
774
|
+
// package is never cleared on weaker isolation than was demanded. Each blocked scan is
|
|
775
|
+
// ledgered (sandbox_inconclusive); the console warning is throttled to once/process.
|
|
776
|
+
if (!_sandboxRuntimeWarned) {
|
|
777
|
+
_sandboxRuntimeWarned = true;
|
|
778
|
+
console.warn(`[SANDBOX] SECURITY: refusing to run — ${rt.reason}. Install gVisor (scripts/install-gvisor.sh) or unset MUADDIB_REQUIRE_GVISOR to allow runc.`);
|
|
736
779
|
}
|
|
780
|
+
return {
|
|
781
|
+
score: -1,
|
|
782
|
+
severity: 'INCONCLUSIVE',
|
|
783
|
+
findings: [{ type: 'gvisor_required_unavailable', severity: 'MEDIUM', detail: rt.reason, evidence: rt.reason }],
|
|
784
|
+
raw_report: null,
|
|
785
|
+
suspicious: false,
|
|
786
|
+
inconclusive: true
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
if (rt.warn && !_sandboxRuntimeWarned) {
|
|
790
|
+
// Fail OPEN but LOUD (was a plain console.log before, or nothing when the env var was
|
|
791
|
+
// simply unset on a gVisor-capable host): surface the weaker-than-possible isolation
|
|
792
|
+
// once per process so it is detectable in production logs.
|
|
793
|
+
_sandboxRuntimeWarned = true;
|
|
794
|
+
console.warn(`[SANDBOX] WARNING: ${rt.reason}. Set MUADDIB_REQUIRE_GVISOR=1 to fail closed instead of running on runc.`);
|
|
795
|
+
} else if (useGvisor) {
|
|
796
|
+
console.log('[SANDBOX] Runtime: gvisor (runsc)');
|
|
737
797
|
}
|
|
738
798
|
|
|
739
799
|
// Generate canary tokens for this sandbox session
|
|
@@ -1177,4 +1237,4 @@ function displayResults(result) {
|
|
|
1177
1237
|
}
|
|
1178
1238
|
}
|
|
1179
1239
|
|
|
1180
|
-
module.exports = { buildSandboxImage, runSandbox, runSingleSandbox, buildDockerArgs, generateFakeHostname, scoreFindings, generateNetworkReport, EXFIL_PATTERNS, SAFE_DOMAINS, getSeverity, displayResults, isDockerAvailable, imageExists, isGvisorAvailable, STATIC_CANARY_TOKENS, detectStaticCanaryExfiltration, analyzePreloadLog, TIME_OFFSETS, SAFE_SANDBOX_CMDS, SANDBOX_CONCURRENCY_MAX, acquireSandboxSlot, tryAcquireSandboxSlot, releaseSandboxSlot, resetSandboxLimiter, getSandboxSemaphore, killAllSandboxContainers };
|
|
1240
|
+
module.exports = { buildSandboxImage, runSandbox, runSingleSandbox, buildDockerArgs, generateFakeHostname, scoreFindings, generateNetworkReport, EXFIL_PATTERNS, SAFE_DOMAINS, getSeverity, displayResults, isDockerAvailable, imageExists, isGvisorAvailable, resolveSandboxRuntime, STATIC_CANARY_TOKENS, detectStaticCanaryExfiltration, analyzePreloadLog, TIME_OFFSETS, SAFE_SANDBOX_CMDS, SANDBOX_CONCURRENCY_MAX, acquireSandboxSlot, tryAcquireSandboxSlot, releaseSandboxSlot, resetSandboxLimiter, getSandboxSemaphore, killAllSandboxContainers };
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
const fs = require('fs');
|
|
32
32
|
const path = require('path');
|
|
33
33
|
const { countInvisibleUnicode, stripInvisibleUnicode } = require('../shared/unicode-invisibles.js');
|
|
34
|
+
const { detectAnalyzerHoneytoken } = require('./ast-detectors/anti-evasion.js');
|
|
34
35
|
|
|
35
36
|
const MAX_FILE_SIZE = 1024 * 1024; // 1 MB cap, cohérent avec ai-config.js
|
|
36
37
|
|
|
@@ -216,6 +217,38 @@ function detectForkExecInlineInterpreter(content) {
|
|
|
216
217
|
return FORK_EXEC_INLINE_INTERPRETER_RE.test(content);
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
// ── Python anti-evasion (M2 follow-up, 2026-07): PyPI parity for AST-096 ──
|
|
221
|
+
// analyzer_honeytoken_reference. Two FP-safe forms mirroring the JS side
|
|
222
|
+
// (ast-detectors/anti-evasion.js), BOTH reusing the existing CRITICAL AST-096
|
|
223
|
+
// rule (no new rule → rule count unchanged):
|
|
224
|
+
// 1. os.environ ENUMERATION (for-in / .keys() / .items()) coupled with a
|
|
225
|
+
// MUADDIB-prefix key test (.startswith / == / in). NOT a direct read:
|
|
226
|
+
// MUAD'DIB tooling reads specific MUADDIB_* config vars directly (verified
|
|
227
|
+
// across the repo), while evasion enumerates the whole env looking for our
|
|
228
|
+
// sandbox tripwire prefix. FP-safe by the distinctive marker + enumeration.
|
|
229
|
+
// 2. base64 / hex / charcode-encoded analyzer marker — handled by REUSING the
|
|
230
|
+
// shared detectAnalyzerHoneytoken() (language-agnostic decoder; it already
|
|
231
|
+
// matches Python byte-array, base64 and hex-escape encodings of the marker).
|
|
232
|
+
// Deliberately NOT covered (honest blind spots): plaintext direct reads of the
|
|
233
|
+
// honeytoken (legit security tooling references markers in the clear — same
|
|
234
|
+
// stance as the JS side); /proc fingerprinting such as reading the kernel
|
|
235
|
+
// version file (legit container-detection reads it — the Python equivalent of
|
|
236
|
+
// the gVisor FP); and shell/bash install hooks (this scanner only reads .py).
|
|
237
|
+
const PY_ENV_ENUM_RE = /\bfor\s+\w+\s+in\s+os\.environ\b|\bos\.environ\.(?:keys|items)\s*\(\s*\)/;
|
|
238
|
+
const PY_ENV_MARKER_TEST_RE = /\.startswith\s*\(\s*['"]MUADDIB|['"]MUADDIB\w*['"]\s*(?:==|!=)|(?:==|!=)\s*['"]MUADDIB|['"]MUADDIB\w*['"]\s+in\b/;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Python parity for the JS detectEnvMarkerEnumeration. Returns 'MUADDIB' if the
|
|
242
|
+
* file enumerates os.environ AND tests keys against the distinctive MUADDIB
|
|
243
|
+
* prefix, else null. FP-safe: MUAD'DIB reads its own MUADDIB_* vars directly
|
|
244
|
+
* (os.environ.get), never via enumerate+startswith (verified across the repo).
|
|
245
|
+
*/
|
|
246
|
+
function detectPythonEnvMarkerEnumeration(content) {
|
|
247
|
+
if (!content || content.indexOf('os.environ') === -1) return null;
|
|
248
|
+
if (!PY_ENV_ENUM_RE.test(content)) return null;
|
|
249
|
+
return PY_ENV_MARKER_TEST_RE.test(content) ? 'MUADDIB' : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
219
252
|
/**
|
|
220
253
|
* Scan Python source files under targetPath for import-time / install-time RCE.
|
|
221
254
|
*
|
|
@@ -347,6 +380,21 @@ function scanPythonSource(targetPath) {
|
|
|
347
380
|
file: relPath
|
|
348
381
|
});
|
|
349
382
|
}
|
|
383
|
+
|
|
384
|
+
// Anti-evasion (AST-096 reused) — Python parity. Runs on cleaned content so
|
|
385
|
+
// a base64 example inside a stripped docstring/comment cannot fire.
|
|
386
|
+
const honeytokenMarker = detectAnalyzerHoneytoken(cleaned);
|
|
387
|
+
const envEnumMarker = honeytokenMarker ? null : detectPythonEnvMarkerEnumeration(cleaned);
|
|
388
|
+
if (honeytokenMarker || envEnumMarker) {
|
|
389
|
+
threats.push({
|
|
390
|
+
type: 'analyzer_honeytoken_reference',
|
|
391
|
+
severity: 'CRITICAL',
|
|
392
|
+
message: honeytokenMarker
|
|
393
|
+
? `${relPath}: obfuscated (base64/hex/charcode) reference to the analysis-environment honeytoken '${honeytokenMarker}' — sandbox-evasion reconnaissance. No legitimate PyPI package encodes a check for this tripwire.`
|
|
394
|
+
: `${relPath}: os.environ enumeration testing keys against a MUADDIB prefix (marker-agnostic) — sandbox/analyzer evasion. No legitimate package scans the environment for this tripwire.`,
|
|
395
|
+
file: relPath
|
|
396
|
+
});
|
|
397
|
+
}
|
|
350
398
|
}
|
|
351
399
|
|
|
352
400
|
return threats;
|
|
@@ -366,6 +414,7 @@ module.exports = {
|
|
|
366
414
|
detectBase64Decode,
|
|
367
415
|
detectDeserialization,
|
|
368
416
|
detectDynamicDangerousImport,
|
|
369
|
-
detectForkExecInlineInterpreter
|
|
417
|
+
detectForkExecInlineInterpreter,
|
|
418
|
+
detectPythonEnvMarkerEnumeration
|
|
370
419
|
}
|
|
371
420
|
};
|