Files
NetMesh/domain/terminalPromptSecurity.test.ts

145 lines
5.7 KiB
TypeScript
Raw Normal View History

import { createRequire } from 'node:module';
import assert from 'node:assert/strict';
import test from 'node:test';
import {
appendTerminalPromptSecurityTail,
isConfirmedTerminalShellPrompt,
isSensitiveTerminalChallenge,
isUntrustedTerminalInputPrompt,
shouldUsePluginTerminalCompletionProvider,
} from './terminalPromptSecurity.ts';
const require = createRequire(import.meta.url);
const sharedPromptSecurity = require('./terminalPromptSecurity.shared.cjs') as {
isUntrustedTerminalInputPrompt: typeof isUntrustedTerminalInputPrompt;
};
test('terminal prompt security recognizes password, MFA, OTP, PIN, and CJK challenges', () => {
const challenges = [
'Password: ',
'Enter passphrase> ',
'OTP> 123456',
'One-time password: 123456',
'Verification code> 123456',
'Duo passcode: 123456',
'MFA token: 123456',
'Authentication code: 123456',
'Security PIN: 123456',
'验证码> 123456',
'动态口令123456',
'二次认证密码123456',
];
for (const challenge of challenges) {
assert.equal(isSensitiveTerminalChallenge(challenge), true, challenge);
}
assert.equal(isSensitiveTerminalChallenge('password authentication succeeded'), false);
assert.equal(isSensitiveTerminalChallenge('echo OTP'), false);
});
test('terminal prompt security combines split output chunks within a bounded logical line', () => {
let tail = appendTerminalPromptSecurityTail('', '\u001b[33mVerifi');
tail = appendTerminalPromptSecurityTail(tail, 'cation code\u001b[0m> ');
assert.equal(isSensitiveTerminalChallenge(tail), true);
tail = appendTerminalPromptSecurityTail(tail, '\r\nuser@host:~$ ');
assert.equal(isSensitiveTerminalChallenge(tail), false);
assert.ok(tail.length <= 2_048);
});
test('terminal prompt security positively confirms shell prompts and fails closed on ambiguous prompts', () => {
const confirmed = [
'$ ',
'root# ',
'user@host:~/repo$ ',
'➜ ~/repo ',
'\uE0B0 ',
'PS C:\\Users\\alice> ',
'C:\\Users\\alice> ',
'user@host> ',
];
for (const prompt of confirmed) {
assert.equal(isConfirmedTerminalShellPrompt(prompt), true, prompt);
}
assert.equal(isConfirmedTerminalShellPrompt('OTP> '), false);
assert.equal(isConfirmedTerminalShellPrompt('custom> '), false);
assert.equal(isConfirmedTerminalShellPrompt('router> ', { allowHostStyleGreaterThan: true }), true);
});
test('plugin terminal completion policy requires a non-sensitive confirmed shell prompt', () => {
assert.equal(shouldUsePluginTerminalCompletionProvider({
sensitiveInputActive: false,
promptText: 'user@host:~$ ',
}), true);
assert.equal(shouldUsePluginTerminalCompletionProvider({
sensitiveInputActive: true,
promptText: 'user@host:~$ ',
}), false);
assert.equal(shouldUsePluginTerminalCompletionProvider({
sensitiveInputActive: false,
promptText: 'OTP> ',
}), false);
assert.equal(shouldUsePluginTerminalCompletionProvider({
sensitiveInputActive: false,
promptText: 'custom> ',
}), false);
});
test('unknown prompt-shaped authentication boundaries fail closed', () => {
assert.equal(isUntrustedTerminalInputPrompt('Custom authentication> '), true);
assert.equal(isUntrustedTerminalInputPrompt('Please authenticate: '), true);
assert.equal(isUntrustedTerminalInputPrompt('alice@host:~$ '), false);
assert.equal(isUntrustedTerminalInputPrompt('router> ', { allowHostStyleGreaterThan: true }), false);
assert.equal(isUntrustedTerminalInputPrompt('router> '), true);
});
test('ordinary shell commands ending with a colon are not untrusted input prompts (#2709)', () => {
// Broadcast pauses while passwordPromptActive is sticky. Typing `lsof -i:`
// must not trip the fail-closed colon heuristic, or peers miss the rest.
const commandLines = [
'user@host:~$ lsof -i:',
'bash-5.2$ lsof -i:',
'root# lsof -i:',
'PS C:\\Users\\alice> lsof -i:',
'C:\\Users\\alice> lsof -i:',
];
for (const line of commandLines) {
assert.equal(isUntrustedTerminalInputPrompt(line), false, line);
}
assert.equal(
isUntrustedTerminalInputPrompt('router> show ip:', { allowHostStyleGreaterThan: true }),
false,
);
// Bare host-style prompts stay fail-closed unless explicitly allowed.
assert.equal(isUntrustedTerminalInputPrompt('router> show ip:'), true);
// Mid-label `#` is not a prompt boundary (no whitespace after `#`).
assert.equal(isUntrustedTerminalInputPrompt('Challenge #1:'), true);
// Markers inside English challenge labels must not look like a shell PS1.
assert.equal(isUntrustedTerminalInputPrompt('Challenge # 1:'), true);
assert.equal(isUntrustedTerminalInputPrompt('Account $ code:'), true);
// Standalone auth-shaped lines without a confirmed shell prompt still fail closed.
assert.equal(isUntrustedTerminalInputPrompt('Please authenticate: '), true);
assert.equal(isUntrustedTerminalInputPrompt('Token: '), true);
assert.equal(isUntrustedTerminalInputPrompt('Password: '), true);
});
test('renderer and main-process prompt classifiers stay aligned', () => {
const cases: Array<[
string,
{ allowHostStyleGreaterThan?: boolean } | undefined,
boolean,
]> = [
['Custom authentication> ', undefined, true],
['Please authenticate: ', undefined, true],
['alice@host:~$ ', undefined, false],
['user@host:~$ lsof -i:', undefined, false],
['Challenge # 1:', undefined, true],
['router> show ip:', { allowHostStyleGreaterThan: true }, false],
['router> show ip:', undefined, true],
];
for (const [prompt, options, expected] of cases) {
assert.equal(isUntrustedTerminalInputPrompt(prompt, options), expected, prompt);
assert.equal(sharedPromptSecurity.isUntrustedTerminalInputPrompt(prompt, options), expected, prompt);
}
});