aboutsummaryrefslogtreecommitdiff
path: root/scripts/test-security.mjs
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-09-29 15:56:27 +0200
committerAgando <roduccomm@gmail.com>2026-09-29 15:56:27 +0200
commit7b9dd97689db400f2408ed50b04857d7d73d83e5 (patch)
treeb1325c677c908f908fec695aafa3a696fd9e69e8 /scripts/test-security.mjs
parent25adb2f8bb96ac1e534645083e02557875af8b67 (diff)
newOS4 v4.6.15: reentrant print-capture stack, AI tail -f block, read-only sed in safeRead, security test harness (21 checks)
Diffstat (limited to 'scripts/test-security.mjs')
-rw-r--r--scripts/test-security.mjs194
1 files changed, 194 insertions, 0 deletions
diff --git a/scripts/test-security.mjs b/scripts/test-security.mjs
new file mode 100644
index 0000000..f94a4fe
--- /dev/null
+++ b/scripts/test-security.mjs
@@ -0,0 +1,194 @@
+#!/usr/bin/env node
+// Test harness for newOS4 WebX11SecurityManager (checkAndRun, suggest, levels, AI loop guard).
+// Loads the security + terminal class source in a vm sandbox with a stub terminal.
+// Run: node scripts/test-security.mjs (exit 1 on failure)
+
+import { readFileSync } from 'node:fs';
+import vm from 'node:vm';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const SRC = readFileSync(path.join(ROOT, 'modules/core/terminal.js'), 'utf8');
+
+function makeCtx() {
+ const ctx = {
+ setInterval, clearInterval, setTimeout, clearTimeout, console,
+ localStorage: { getItem: () => null, setItem: () => {}, removeItem: () => {} },
+ document: {
+ createElement: () => ({ style: {}, appendChild: () => {}, classList: { add: () => {} }, setAttribute: () => {}, addEventListener: () => {} }),
+ getElementById: () => null,
+ addEventListener: () => {},
+ },
+ window: {},
+ };
+ ctx.window = ctx;
+ vm.createContext(ctx);
+ // terminal.js defines classes inside an IIFE/module scope — expose them for the test
+ vm.runInContext(SRC + '\n;globalThis.__WebX11SecurityManager = (typeof WebX11SecurityManager !== "undefined") ? WebX11SecurityManager : null;', ctx, { filename: 'terminal.js' });
+ return ctx;
+}
+
+let pass = 0, fail = 0;
+const check = (name, cond, got) => {
+ if (cond) { pass++; console.log(' ok ' + name); }
+ else { fail++; console.log(' FAIL ' + name + (got !== undefined ? ' — got: ' + JSON.stringify(got) : '')); }
+};
+
+const ctx = makeCtx();
+if (!ctx.__WebX11SecurityManager) {
+ console.error('WebX11SecurityManager not reachable at module scope — class is file-scoped.');
+ console.error('Check how terminal.js wraps the class and expose it for tests.');
+ process.exit(2);
+}
+
+// Stub terminal: capture print calls, provide the module registry shape security expects
+function makeStubTerminal() {
+ const out = [];
+ return {
+ out,
+ print: (t, ty) => out.push({ text: String(t), type: ty || '' }),
+ moduleManager: { modules: {
+ // A module with commands so _collectAllCommands finds them (tail needed for typo tests)
+ 'FsModule': { commands: { 'ls': () => {}, 'cat': () => {}, 'grep': () => {}, 'tail': () => {}, 'head': () => {}, 'sed': () => {}, 'sort': () => {} } },
+ } },
+ security: null, // set by caller
+ showConfirmationBar: null,
+ showSuggestionBar: null,
+ _tryAIRoute: null,
+ _dispatch: null,
+ };
+}
+
+function makeSec(level, stubTerminal) {
+ const sec = new ctx.__WebX11SecurityManager(stubTerminal, level);
+ stubTerminal.security = sec;
+ return sec;
+}
+
+const text = (out) => out.map(o => o.text).join('\n');
+
+// ─── construction & base state ───
+{
+ const t = makeStubTerminal();
+ const sec = makeSec(1, t);
+ check('constructs at level 1', sec.level === 1);
+ check('depth counter starts at 0', sec._aiToolDepth === 0);
+ check('suggest finds tail for taill', JSON.stringify(sec.suggest('taill')) === '{"cmd":"tail","dist":1}', sec.suggest('taill'));
+ check('suggest null for gibberish', sec.suggest('zzzzzzzzz') === null);
+}
+
+// ─── level 2: unknown → blocked, no AI ───
+{
+ const t = makeStubTerminal();
+ const sec = makeSec(2, t);
+ let aiRouted = false;
+ sec._tryAIRoute = () => { aiRouted = true; };
+ sec._dispatch = () => {};
+ await sec.checkAndRun('foobarxyz');
+ check('L2 unknown blocked message', text(t.out).includes('blocked (level 2)'), text(t.out));
+ check('L2 unknown does not route to AI', aiRouted === false);
+}
+
+// ─── level 1 + AI depth: unknown → structured error, no bar, no AI route ───
+{
+ const t = makeStubTerminal();
+ const sec = makeSec(1, t);
+ sec._aiToolDepth = 1; // simulate AI tool call
+ let aiRouted = false;
+ sec._tryAIRoute = () => { aiRouted = true; };
+ sec._dispatch = () => {};
+ await sec.checkAndRun('foobarxyz');
+ check('AI-depth unknown returns "Command not found"', text(t.out).includes('Command not found: foobarxyz'), text(t.out));
+ check('AI-depth unknown does not route to AI', aiRouted === false);
+
+ const t2 = makeStubTerminal();
+ const sec2 = makeSec(1, t2);
+ sec2._aiToolDepth = 2; // nested depth
+ let aiRouted2 = false;
+ sec2._tryAIRoute = () => { aiRouted2 = true; };
+ sec2._dispatch = () => {};
+ await sec2.checkAndRun('taill x.txt');
+ check('nested AI-depth typo returns error + suggestion', text(t2.out).includes('Command not found: taill') && text(t2.out).includes('tail'), text(t2.out));
+ check('nested AI-depth does not show suggestion bar', !t2.out.some(o => o.text.includes('did you mean:')), t2.out.map(o => o.text));
+ check('nested AI-depth does not route to AI', aiRouted2 === false);
+}
+
+// ─── level 1 user path: typo → suggestion bar (stub has no showSuggestionBar → fallback message) ───
+{
+ const t = makeStubTerminal();
+ const sec = makeSec(1, t);
+ let aiRouted = false;
+ sec._tryAIRoute = () => { aiRouted = true; };
+ sec._dispatch = () => {};
+ // stub terminal lacks showSuggestionBar → code takes AI-route fallback... verify current guard
+ await sec.checkAndRun('taill x.txt');
+ // With showSuggestionBar missing, we expect routing to AI (bar unavailable)
+ check('user typo without bar API routes to AI', aiRouted === true);
+}
+
+// ─── level 1 user path with bar API: typo → bar, no AI ───
+{
+ const t = makeStubTerminal();
+ t.showSuggestionBar = (cmd, args) => { t.barShown = { cmd, args }; };
+ const sec = makeSec(1, t);
+ let aiRouted = false;
+ sec._tryAIRoute = () => { aiRouted = true; };
+ sec._dispatch = () => {};
+ await sec.checkAndRun('taill x.txt');
+ check('user typo with bar API shows bar', t.barShown && t.barShown.cmd === 'tail', t.barShown);
+ check('user typo with bar API does not route to AI', aiRouted === false);
+}
+
+// ─── level 1 user path: real unknown → AI route ───
+{
+ const t = makeStubTerminal();
+ const sec = makeSec(1, t);
+ let aiRouted = false;
+ sec._tryAIRoute = () => { aiRouted = true; };
+ sec._dispatch = () => {};
+ await sec.checkAndRun('foobarxyz');
+ check('user real-unknown routes to AI', aiRouted === true);
+ check('user real-unknown message says routing', text(t.out).includes('routing to AI'), text(t.out));
+}
+
+// ─── known whitelisted command dispatches normally ───
+{
+ const t = makeStubTerminal();
+ const sec = makeSec(1, t);
+ let dispatched = null;
+ sec._dispatch = (cmd, args) => { dispatched = { cmd, args }; };
+ await sec.checkAndRun('sed s/a/b/ file.txt');
+ check('whitelisted sed dispatches', dispatched && dispatched.cmd === 'sed', dispatched);
+ check('no security messages for known cmd', t.out.length === 0, t.out);
+}
+
+// ─── AI depth does not leak across sequential calls ───
+{
+ const t = makeStubTerminal();
+ const sec = makeSec(1, t);
+ sec._tryAIRoute = () => {};
+ sec._dispatch = () => {};
+ sec._aiToolDepth = 1;
+ await sec.checkAndRun('nope1');
+ check('depth still 1 after first call', sec._aiToolDepth === 1);
+ sec._aiToolDepth = 0;
+ let aiRouted = false;
+ sec._tryAIRoute = () => { aiRouted = true; };
+ await sec.checkAndRun('nope2');
+ check('depth 0 → routes to AI again', aiRouted === true);
+}
+
+// ─── L2 blocked message with suggestion ───
+{
+ const t = makeStubTerminal();
+ const sec = makeSec(2, t);
+ sec._tryAIRoute = () => {};
+ sec._dispatch = () => {};
+ await sec.checkAndRun('taill x.txt');
+ check('L2 block includes did-you-mean', text(t.out).includes('Did you mean: "tail"?'), text(t.out));
+}
+
+console.log('');
+console.log(`${pass} passed, ${fail} failed`);
+process.exit(fail ? 1 : 0); \ No newline at end of file