Intensive end-to-end testing (a live gateway-MCP <-> agent integration harness +
an adversarial redaction/abuse probe + an independent security audit) surfaced
three real issues in the shipped read-only diagnostic agent. All run in lib/**,
which is packaged in the app.
1. grep ReDoS froze the Electron main process. read_log compiled the
client-supplied grep into `new RegExp(grep, 'i')` and ran it synchronously over
the log tail IN the main process. A catastrophic pattern (e.g. "(a+)+$" against
a long line) hangs the whole app — empirically confirmed (8s timeout, killed).
JS regex is synchronous and uncancellable, so grep is now a case-insensitive
literal substring filter with "|" alternation ("error|timeout|502"). Provably
linear-time; covers the real diagnostic need.
2. Prototype-chain whitelist bypass. The op table was a plain object literal, so
handle("constructor" | "toString" | "valueOf", ...) resolved an inherited
Object.prototype function, passed the `typeof fn === 'function'` guard and
returned {ok:true}. Harmless functions today, but a whitelist-integrity hole.
Now guarded with a string check + Object.prototype.hasOwnProperty.
3. Redaction defense-in-depth gaps. redactLogText now also scrubs: basic-auth URL
passwords (scheme://user:pass@host), Authorization: Basic, JWTs (eyJ...x.y.z),
and bare/JSON session= values. Mostly theoretical in today's readable logs
(secret-bearing bodies go to the excluded doodstream-debug.log; other hosters
throw static strings) but matters as the verbose-logging surface grows.
Verified: 383 app tests (incl. new regression tests for all three), the live
gateway-MCP integration harness (all 14 tools, zero leaks, error paths), the
adversarial probe (14/14+ secret shapes scrubbed, ReDoS 1ms, lockout, malformed
args), e2e gate, lint 0 errors. Only residual: a standalone high-entropy blob with
zero key/Bearer/URL context — inherent to any denylist, acknowledged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
33 lines
1.2 KiB
JavaScript
33 lines
1.2 KiB
JavaScript
function createAgent(collectors) {
|
|
const OPS = {
|
|
get_system_info: (a) => collectors.getSystemInfo(a),
|
|
server_health: (a) => collectors.serverHealth(a),
|
|
get_config_redacted: (a) => collectors.getConfigRedacted(a),
|
|
list_logs: () => collectors.listLogs(),
|
|
read_log: (a) => collectors.readLog(a),
|
|
tail_log: (a) => collectors.readLog(a),
|
|
get_app_events: (a) => collectors.getAppEvents(a),
|
|
list_errors: (a) => collectors.listErrors(a),
|
|
get_queue_state: (a) => collectors.getQueueState(a),
|
|
get_history: (a) => collectors.getHistory(a),
|
|
get_rotation_state: () => collectors.getRotationState(),
|
|
get_health: () => collectors.getHealth()
|
|
};
|
|
|
|
function handle(op, args) {
|
|
const fn = (typeof op === 'string' && Object.prototype.hasOwnProperty.call(OPS, op)) ? OPS[op] : null;
|
|
if (typeof fn !== 'function') return { ok: false, error: `unknown or non-readonly op: ${op}` };
|
|
try {
|
|
const data = fn(args || {});
|
|
if (data && data.ok === false) return data;
|
|
return { ok: true, data };
|
|
} catch (e) {
|
|
return { ok: false, error: String((e && e.message) || e) };
|
|
}
|
|
}
|
|
|
|
return { handle, ops: Object.keys(OPS) };
|
|
}
|
|
|
|
module.exports = { createAgent };
|