@goodandready/dsh-moa 0.2.49 → 0.2.51
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/lib/history.js +12 -11
- package/lib/moa-test-gate.js +27 -42
- package/package.json +1 -1
package/lib/history.js
CHANGED
|
@@ -53,7 +53,7 @@ export function invalidateHistoryCache() {
|
|
|
53
53
|
|
|
54
54
|
function getOrInitCache(filePath, stat) {
|
|
55
55
|
if (!_historyCache || _historyCache.filePath !== filePath || _historyCache.mtimeMs !== stat.mtimeMs || _historyCache.size !== stat.size) {
|
|
56
|
-
_historyCache = { filePath, mtimeMs: stat.mtimeMs, size: stat.size, leaderboards: new Map(), runsById: new Map(), totalCount: null }
|
|
56
|
+
_historyCache = { filePath, mtimeMs: stat.mtimeMs, size: stat.size, leaderboards: new Map(), runsById: new Map(), totalCount: null, fullyIndexed: false }
|
|
57
57
|
}
|
|
58
58
|
if (!_historyCache.runsById) _historyCache.runsById = new Map()
|
|
59
59
|
return _historyCache
|
|
@@ -473,19 +473,20 @@ export function getMoaRunById(runId, filePath = getDefaultHistoryFile()) {
|
|
|
473
473
|
const stat = fs.statSync(filePath)
|
|
474
474
|
const cache = getOrInitCache(filePath, stat)
|
|
475
475
|
if (cache.runsById.has(runId)) return cache.runsById.get(runId)
|
|
476
|
+
if (cache.fullyIndexed) return null
|
|
476
477
|
|
|
477
478
|
if (stat.size <= 65536) {
|
|
478
479
|
const raw = fs.readFileSync(filePath, 'utf8')
|
|
479
|
-
const lines = raw.trim().split(
|
|
480
|
+
const lines = raw.trim().split(String.fromCharCode(10)).filter(Boolean)
|
|
480
481
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
481
482
|
try {
|
|
482
483
|
const item = JSON.parse(lines[i])
|
|
483
|
-
if (item?.id)
|
|
484
|
-
cache.runsById.set(item.id, item)
|
|
485
|
-
if (item.id === runId) return item
|
|
486
|
-
}
|
|
484
|
+
if (item?.id) cache.runsById.set(item.id, item)
|
|
487
485
|
} catch { /* skip corrupt */ }
|
|
488
486
|
}
|
|
487
|
+
cache.fullyIndexed = true
|
|
488
|
+
if (cache.runsById.has(runId)) return cache.runsById.get(runId)
|
|
489
|
+
cache.runsById.set(runId, null)
|
|
489
490
|
return null
|
|
490
491
|
}
|
|
491
492
|
|
|
@@ -501,16 +502,16 @@ export function getMoaRunById(runId, filePath = getDefaultHistoryFile()) {
|
|
|
501
502
|
}
|
|
502
503
|
|
|
503
504
|
const raw = fs.readFileSync(filePath, 'utf8')
|
|
504
|
-
const lines = raw.trim().split(
|
|
505
|
+
const lines = raw.trim().split(String.fromCharCode(10)).filter(Boolean)
|
|
505
506
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
506
507
|
try {
|
|
507
508
|
const item = JSON.parse(lines[i])
|
|
508
|
-
if (item?.id)
|
|
509
|
-
cache.runsById.set(item.id, item)
|
|
510
|
-
if (item.id === runId) return item
|
|
511
|
-
}
|
|
509
|
+
if (item?.id) cache.runsById.set(item.id, item)
|
|
512
510
|
} catch { /* skip corrupt */ }
|
|
513
511
|
}
|
|
512
|
+
cache.fullyIndexed = true
|
|
513
|
+
if (cache.runsById.has(runId)) return cache.runsById.get(runId)
|
|
514
|
+
cache.runsById.set(runId, null)
|
|
514
515
|
return null
|
|
515
516
|
} catch {
|
|
516
517
|
return null
|
package/lib/moa-test-gate.js
CHANGED
|
@@ -45,21 +45,12 @@ export function resolveCandidateDir(baseDir, candidateIndex, options = {}) {
|
|
|
45
45
|
|
|
46
46
|
export function decodeEscapedStrings(str) {
|
|
47
47
|
if (!str || typeof str !== 'string') return ''
|
|
48
|
-
|
|
48
|
+
return str.replace(/\\+/g, '\\').replace(/\\(?:([0-7]{1,3})|x([0-9a-fA-F]{2}))/g, (m, oct, hex) =>
|
|
49
49
|
oct ? String.fromCharCode(parseInt(oct, 8)) : hex ? String.fromCharCode(parseInt(hex, 16)) : m
|
|
50
50
|
)
|
|
51
|
-
return res.replace(/(?<![A-Za-z0-9+/=])(?:(?:[A-Za-z0-9+/]{4}){2,}|(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))(?![A-Za-z0-9+/=])/g, (m) => {
|
|
52
|
-
try {
|
|
53
|
-
const dec = Buffer.from(m, 'base64').toString('utf8')
|
|
54
|
-
return /^[\x20-\x7E\r\n\t]+$/.test(dec) ? dec : m
|
|
55
|
-
} catch (err) {
|
|
56
|
-
logger.debug('[dsh-moa:testGate] base64 decode skip:', err?.message || err)
|
|
57
|
-
return m
|
|
58
|
-
}
|
|
59
|
-
})
|
|
60
51
|
}
|
|
61
52
|
|
|
62
|
-
const COMPUTED_SHELL_EVASION = /(?:base64\s+(?:-d|--decode|-D)[^|\n;]*\|\s*(?:ba|z|da)?sh|\beval\s+["
|
|
53
|
+
const COMPUTED_SHELL_EVASION = /(?:base64\s+(?:-d|--decode|-D)[^|\n;]*\|\s*(?:ba|z|da)?sh|\beval\s+["']?\$|\bxxd\s+-r[^|\n;]*\|\s*(?:ba|z|da)?sh|\bprintf\s+[^|\n]+\|\s*(?:ba|z|da)?sh|\bopenssl\s+enc\s+-d[^|\n;]*\|\s*(?:ba|z|da)?sh|\bprintf\s+(?:%s|["']%s["'])\s+.*["'](?:\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\$\(\s*(?:printf|echo)\s+.*["'](?:\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\x60\s*(?:printf|echo)\s+.*["'](?:\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\b(?:cat|head|tail|source|\.|touch|rm|cp|mv)\s+["']?\$[A-Za-z_]|>\s*["']?\$[A-Za-z_]|>>\s*["']?\$[A-Za-z_])/i
|
|
63
54
|
|
|
64
55
|
const UNIX_ROOT_PREFIX = /^\/(?:home|root|etc|var|tmp|opt|mnt|srv|media|proc|sys|dev|usr|bin|lib|lib64|run\/(?:user\/\d+|systemd|lock|credentials|secrets|udev|initramfs|mount|shm|dbus|sshd|motd)|sbin|boot|Users\/(?:[a-zA-Z0-9_\-.]+)|Applications|Library|System|Volumes|private)(?:\/|$)/
|
|
65
56
|
|
|
@@ -82,6 +73,12 @@ function scanPathTokensForLeak(text, allowedRoots, executionDir) {
|
|
|
82
73
|
}
|
|
83
74
|
const rawLeak = checkText(text)
|
|
84
75
|
if (rawLeak) return rawLeak
|
|
76
|
+
const segmented = text.match(/\bprintf\s+(?:%s|['"]%s['"])\s+((?:['"][^'"]*['"]\s*)+)/i)
|
|
77
|
+
if (segmented) {
|
|
78
|
+
const assembled = (segmented[1].match(/['"]([^'"]*)['"]/g) || []).map((s) => s.slice(1, -1)).join('')
|
|
79
|
+
const assembledLeak = checkText(assembled)
|
|
80
|
+
if (assembledLeak) return assembledLeak
|
|
81
|
+
}
|
|
85
82
|
const decoded = decodeEscapedStrings(text)
|
|
86
83
|
return decoded !== text ? checkText(decoded) : null
|
|
87
84
|
}
|
|
@@ -166,6 +163,18 @@ export async function runCandidateTestGate({
|
|
|
166
163
|
stderr = customRes?.stderr || ''
|
|
167
164
|
} else {
|
|
168
165
|
const allowedRoots = [executionDir, cwd, ...(candidateDir && candidateDir !== executionDir ? [candidateDir] : [])]
|
|
166
|
+
if (COMPUTED_SHELL_EVASION.test(resolvedCmd)) {
|
|
167
|
+
const durationMs = Date.now() - startTime
|
|
168
|
+
return {
|
|
169
|
+
candidateIndex,
|
|
170
|
+
passed: false,
|
|
171
|
+
exitCode: 1,
|
|
172
|
+
durationMs,
|
|
173
|
+
command: resolvedCmd,
|
|
174
|
+
output: 'Access denied: test command contains obfuscated or computed command execution',
|
|
175
|
+
summary: `FAIL (code 1, ${durationMs}ms)`,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
169
178
|
const cmdTokens = resolvedCmd.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []
|
|
170
179
|
|
|
171
180
|
// 1. Check command tokens for absolute path leaks
|
|
@@ -310,7 +319,7 @@ function decodeEscaped(str) {
|
|
|
310
319
|
return m;
|
|
311
320
|
});
|
|
312
321
|
}
|
|
313
|
-
const COMPUTED_SHELL_EVASION = /(?:base64\\s+(?:-d|--decode|-D)[^|\\n;]*\\|\\s*(?:ba|z|da)?sh|\\beval\\s+["
|
|
322
|
+
const COMPUTED_SHELL_EVASION = /(?:base64\\s+(?:-d|--decode|-D)[^|\\n;]*\\|\\s*(?:ba|z|da)?sh|\\beval\\s+["']?\\$|\\bxxd\\s+-r[^|\\n;]*\\|\\s*(?:ba|z|da)?sh|\\bprintf\\s+[^|\\n]+\\|\\s*(?:ba|z|da)?sh|\\bopenssl\\s+enc\\s+-d[^|\\n;]*\\|\\s*(?:ba|z|da)?sh|\\bprintf\\s+(?:%s|["']%s["'])\\s+.*["'](?:\\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\\$\\(\\s*(?:printf|echo)\\s+.*["'](?:\\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\\x60\\s*(?:printf|echo)\\s+.*["'](?:\\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\\b(?:cat|head|tail|source|\\.|touch|rm|cp|mv)\\s+["']?\\$[A-Za-z_]|>\\s*["']?\\$[A-Za-z_]|>>\\s*["']?\\$[A-Za-z_])/i;
|
|
314
323
|
const UNIX_ROOT_PREFIX = /^\\/(?:home|root|etc|var|tmp|opt|mnt|srv|media|proc|sys|dev|usr|bin|lib|lib64|run\\/(?:user\\/\\d+|systemd|lock|credentials|secrets|udev|initramfs|mount|shm|dbus|sshd|motd)|sbin|boot|Users\\/(?:[a-zA-Z0-9_\\-.]+)|Applications|Library|System|Volumes|private)(?:\\/|$)/;
|
|
315
324
|
function checkSinglePath(p) {
|
|
316
325
|
if (!p || typeof p !== 'string') return true;
|
|
@@ -355,36 +364,12 @@ function guardArgs(cmd, args) {
|
|
|
355
364
|
}
|
|
356
365
|
}
|
|
357
366
|
}
|
|
358
|
-
const origSpawn = cp.spawn;
|
|
359
|
-
cp.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
};
|
|
363
|
-
const
|
|
364
|
-
cp.spawnSync = function(cmd, args, opts) {
|
|
365
|
-
guardArgs(cmd, args);
|
|
366
|
-
return origSpawnSync.apply(this, arguments);
|
|
367
|
-
};
|
|
368
|
-
const origExecFile = cp.execFile;
|
|
369
|
-
cp.execFile = function(file, args, opts, cb) {
|
|
370
|
-
guardArgs(file, args);
|
|
371
|
-
return origExecFile.apply(this, arguments);
|
|
372
|
-
};
|
|
373
|
-
const origExecFileSync = cp.execFileSync;
|
|
374
|
-
cp.execFileSync = function(file, args, opts) {
|
|
375
|
-
guardArgs(file, args);
|
|
376
|
-
return origExecFileSync.apply(this, arguments);
|
|
377
|
-
};
|
|
378
|
-
const origExec = cp.exec;
|
|
379
|
-
cp.exec = function(cmd, opts, cb) {
|
|
380
|
-
guardArgs(cmd, [cmd]);
|
|
381
|
-
return origExec.apply(this, arguments);
|
|
382
|
-
};
|
|
383
|
-
const origExecSync = cp.execSync;
|
|
384
|
-
cp.execSync = function(cmd, opts) {
|
|
385
|
-
guardArgs(cmd, [cmd]);
|
|
386
|
-
return origExecSync.apply(this, arguments);
|
|
387
|
-
};
|
|
367
|
+
const origSpawn = cp.spawn; cp.spawn = function(cmd, args, opts) { guardArgs(cmd, args); return origSpawn.apply(this, arguments); };
|
|
368
|
+
const origSpawnSync = cp.spawnSync; cp.spawnSync = function(cmd, args, opts) { guardArgs(cmd, args); return origSpawnSync.apply(this, arguments); };
|
|
369
|
+
const origExecFile = cp.execFile; cp.execFile = function(file, args, opts, cb) { guardArgs(file, args); return origExecFile.apply(this, arguments); };
|
|
370
|
+
const origExecFileSync = cp.execFileSync; cp.execFileSync = function(file, args, opts) { guardArgs(file, args); return origExecFileSync.apply(this, arguments); };
|
|
371
|
+
const origExec = cp.exec; cp.exec = function(cmd, opts, cb) { guardArgs(cmd, [cmd]); return origExec.apply(this, arguments); };
|
|
372
|
+
const origExecSync = cp.execSync; cp.execSync = function(cmd, opts) { guardArgs(cmd, [cmd]); return origExecSync.apply(this, arguments); };
|
|
388
373
|
const fsMod = require('node:fs');
|
|
389
374
|
function guardFs(p, isWrite) {
|
|
390
375
|
if (!p || typeof p !== 'string') return;
|