|
| 1 | +import childProcess from 'child_process'; |
1 | 2 | import test from 'ava'; |
2 | 3 | import { |
| 4 | + buildWindowsNotificationPayload, |
3 | 5 | getNotificationsConfig, |
4 | 6 | sendNotification, |
5 | 7 | setNotificationsConfig, |
@@ -107,6 +109,125 @@ test.serial('sendNotification handles undefined events gracefully', (t) => { |
107 | 109 | t.notThrows(() => sendNotification('toolConfirmation')); |
108 | 110 | }); |
109 | 111 |
|
| 112 | +// ============================================================================ |
| 113 | +// Windows Notification Tests & Security Verification |
| 114 | +// ============================================================================ |
| 115 | + |
| 116 | +test.serial( |
| 117 | + 'buildWindowsNotificationPayload generates static script and out-of-band env vars', |
| 118 | + (t) => { |
| 119 | + const title = 'Test Title `whoami` & $env:TEMP'; |
| 120 | + const message = 'Line 1\nLine 2 with "quotes" and \'single quotes\' 🚀'; |
| 121 | + |
| 122 | + const payload = buildWindowsNotificationPayload(title, message); |
| 123 | + |
| 124 | + t.is(payload.command, 'powershell'); |
| 125 | + t.is(payload.args[0], '-NoProfile'); |
| 126 | + t.is(payload.args[1], '-NonInteractive'); |
| 127 | + t.is(payload.args[2], '-EncodedCommand'); |
| 128 | + t.true(payload.options.windowsHide); |
| 129 | + |
| 130 | + // Verify encoded script decodes properly and does not interpolate user strings |
| 131 | + const decodedScript = Buffer.from(payload.args[3], 'base64').toString( |
| 132 | + 'utf16le', |
| 133 | + ); |
| 134 | + t.true(decodedScript.includes('System.Windows.Forms.NotifyIcon')); |
| 135 | + t.true(decodedScript.includes('$env:NANOCODER_NOTIFICATION_TITLE')); |
| 136 | + t.true(decodedScript.includes('$env:NANOCODER_NOTIFICATION_MESSAGE')); |
| 137 | + |
| 138 | + // Verify title and message env vars are passed verbatim |
| 139 | + t.is(payload.options.env.NANOCODER_NOTIFICATION_TITLE, title); |
| 140 | + t.is(payload.options.env.NANOCODER_NOTIFICATION_MESSAGE, message); |
| 141 | + }, |
| 142 | +); |
| 143 | + |
| 144 | +test.serial( |
| 145 | + 'buildWindowsNotificationPayload safely preserves edge case characters in env vars', |
| 146 | + (t) => { |
| 147 | + const injectionTitles = [ |
| 148 | + '`whoami`', |
| 149 | + '$(Get-Process)', |
| 150 | + '$env:USERPROFILE', |
| 151 | + "'; Remove-Item -Recurse C:\\; '", |
| 152 | + 'Title with "double quotes" and \'single quotes\'', |
| 153 | + 'Emoji 🚀 and Unicode 你好世界', |
| 154 | + ]; |
| 155 | + |
| 156 | + for (const title of injectionTitles) { |
| 157 | + const payload = buildWindowsNotificationPayload(title, 'sample message'); |
| 158 | + |
| 159 | + // Payload env var must match exact literal text |
| 160 | + t.is(payload.options.env.NANOCODER_NOTIFICATION_TITLE, title); |
| 161 | + } |
| 162 | + }, |
| 163 | +); |
| 164 | + |
| 165 | +test.serial( |
| 166 | + 'sendNotification handles win32 platform gracefully and invokes static powershell command', |
| 167 | + (t) => { |
| 168 | + const originalPlatform = process.platform; |
| 169 | + const originalExecFile = childProcess.execFile; |
| 170 | + let executedCommand = ''; |
| 171 | + let executedArgs: string[] = []; |
| 172 | + let executedOptions: {windowsHide?: boolean; env?: NodeJS.ProcessEnv} = {}; |
| 173 | + |
| 174 | + // Spy on childProcess.execFile |
| 175 | + // biome-ignore lint/suspicious/noExplicitAny: test stub |
| 176 | + (childProcess.execFile as any) = ( |
| 177 | + command: string, |
| 178 | + args: string[], |
| 179 | + options: any, |
| 180 | + callback: any, |
| 181 | + ) => { |
| 182 | + executedCommand = command; |
| 183 | + executedArgs = args; |
| 184 | + executedOptions = options; |
| 185 | + if (typeof options === 'function') { |
| 186 | + options(); |
| 187 | + } else if (typeof callback === 'function') { |
| 188 | + callback(); |
| 189 | + } |
| 190 | + }; |
| 191 | + |
| 192 | + Object.defineProperty(process, 'platform', { |
| 193 | + value: 'win32', |
| 194 | + configurable: true, |
| 195 | + }); |
| 196 | + |
| 197 | + try { |
| 198 | + setNotificationsConfig({ |
| 199 | + enabled: true, |
| 200 | + events: {generationComplete: true}, |
| 201 | + customMessages: { |
| 202 | + generationComplete: { |
| 203 | + title: '`whoami`', |
| 204 | + message: 'Test message $(dir)', |
| 205 | + }, |
| 206 | + }, |
| 207 | + }); |
| 208 | + |
| 209 | + t.notThrows(() => sendNotification('generationComplete')); |
| 210 | + |
| 211 | + t.is(executedCommand, 'powershell'); |
| 212 | + t.is(executedArgs[0], '-NoProfile'); |
| 213 | + t.is(executedArgs[1], '-NonInteractive'); |
| 214 | + t.is(executedArgs[2], '-EncodedCommand'); |
| 215 | + t.true(executedOptions.windowsHide); |
| 216 | + t.is(executedOptions.env?.NANOCODER_NOTIFICATION_TITLE, '`whoami`'); |
| 217 | + t.is( |
| 218 | + executedOptions.env?.NANOCODER_NOTIFICATION_MESSAGE, |
| 219 | + 'Test message $(dir)', |
| 220 | + ); |
| 221 | + } finally { |
| 222 | + childProcess.execFile = originalExecFile; |
| 223 | + Object.defineProperty(process, 'platform', { |
| 224 | + value: originalPlatform, |
| 225 | + configurable: true, |
| 226 | + }); |
| 227 | + } |
| 228 | + }, |
| 229 | +); |
| 230 | + |
110 | 231 | // ============================================================================ |
111 | 232 | // Terminal Bell Tests |
112 | 233 | // ============================================================================ |
|
0 commit comments