* Walk a `command` node and extract argv. Children appear in order: * [variable_assignment...] command_name [argument...] [file_redirect...] * Any child type not explicitly handled triggers too-complex.
( node: Node, extraRedirects: Redirect[], innerCommands: SimpleCommand[], varScope: Map<string, string>, )
| 1235 | * Any child type not explicitly handled triggers too-complex. |
| 1236 | */ |
| 1237 | function walkCommand( |
| 1238 | node: Node, |
| 1239 | extraRedirects: Redirect[], |
| 1240 | innerCommands: SimpleCommand[], |
| 1241 | varScope: Map<string, string>, |
| 1242 | ): ParseForSecurityResult { |
| 1243 | const argv: string[] = [] |
| 1244 | const envVars: { name: string; value: string }[] = [] |
| 1245 | const redirects: Redirect[] = [...extraRedirects] |
| 1246 | |
| 1247 | for (const child of node.children) { |
| 1248 | if (!child) continue |
| 1249 | |
| 1250 | switch (child.type) { |
| 1251 | case 'variable_assignment': { |
| 1252 | const ev = walkVariableAssignment(child, innerCommands, varScope) |
| 1253 | if ('kind' in ev) return ev |
| 1254 | // SECURITY: Env-prefix assignments (`VAR=x cmd`) are command-local in |
| 1255 | // bash — VAR is only visible to `cmd` as an env var, NOT to |
| 1256 | // subsequent commands. Do NOT add to global varScope — that would |
| 1257 | // let `VAR=safe cmd1 && rm $VAR` resolve $VAR when bash has unset it. |
| 1258 | envVars.push({ name: ev.name, value: ev.value }) |
| 1259 | break |
| 1260 | } |
| 1261 | case 'command_name': { |
| 1262 | const arg = walkArgument( |
| 1263 | child.children[0] ?? child, |
| 1264 | innerCommands, |
| 1265 | varScope, |
| 1266 | ) |
| 1267 | if (typeof arg !== 'string') return arg |
| 1268 | argv.push(arg) |
| 1269 | break |
| 1270 | } |
| 1271 | case 'word': |
| 1272 | case 'number': |
| 1273 | case 'raw_string': |
| 1274 | case 'string': |
| 1275 | case 'concatenation': |
| 1276 | case 'arithmetic_expansion': { |
| 1277 | const arg = walkArgument(child, innerCommands, varScope) |
| 1278 | if (typeof arg !== 'string') return arg |
| 1279 | argv.push(arg) |
| 1280 | break |
| 1281 | } |
| 1282 | // NOTE: command_substitution as a BARE argument (not inside a string) |
| 1283 | // is intentionally NOT handled here — the $() output IS the argument, |
| 1284 | // and for path-sensitive commands (cd, rm, chmod) the placeholder would |
| 1285 | // hide the real path from downstream checks. `cd $(echo /etc)` must |
| 1286 | // stay too-complex so the path-check can't be bypassed. $() inside |
| 1287 | // strings ("Timer: $(date)") is handled in walkString where the output |
| 1288 | // is embedded in a longer string (safer). |
| 1289 | case 'simple_expansion': { |
| 1290 | // Bare `$VAR` as an argument. Tracked static vars return the ACTUAL |
| 1291 | // value (e.g. VAR=/etc → '/etc'). Values with IFS/glob chars or |
| 1292 | // placeholders reject. See resolveSimpleExpansion. |
| 1293 | const v = resolveSimpleExpansion(child, varScope, false) |
| 1294 | if (typeof v !== 'string') return v |
no test coverage detected