(raw: RawStatement)
| 1000 | /** Transform a raw statement into ParsedStatement */ |
| 1001 | // exported for testing |
| 1002 | export function transformStatement(raw: RawStatement): ParsedStatement { |
| 1003 | const statementType = mapStatementType(raw.type) |
| 1004 | const commands: ParsedCommandElement[] = [] |
| 1005 | const redirections: ParsedRedirection[] = [] |
| 1006 | |
| 1007 | if (raw.elements) { |
| 1008 | // PipelineAst: walk pipeline elements |
| 1009 | for (const elem of ensureArray(raw.elements)) { |
| 1010 | if (elem.type === 'CommandAst') { |
| 1011 | commands.push(transformCommandAst(elem)) |
| 1012 | for (const redir of ensureArray(elem.redirections)) { |
| 1013 | redirections.push(transformRedirection(redir)) |
| 1014 | } |
| 1015 | } else { |
| 1016 | commands.push(transformExpressionElement(elem)) |
| 1017 | // SECURITY: CommandExpressionAst also carries .Redirections (inherited |
| 1018 | // from CommandBaseAst). `1 > /tmp/evil.txt` is a CommandExpressionAst |
| 1019 | // with a FileRedirectionAst. Must extract here or getFileRedirections() |
| 1020 | // misses it and compound commands like `Get-ChildItem; 1 > /tmp/x` |
| 1021 | // auto-allow at step 5 (only Get-ChildItem is checked). |
| 1022 | for (const redir of ensureArray(elem.redirections)) { |
| 1023 | redirections.push(transformRedirection(redir)) |
| 1024 | } |
| 1025 | } |
| 1026 | } |
| 1027 | // SECURITY: The PS1 PipelineAst branch does a deep FindAll for |
| 1028 | // FileRedirectionAst to catch redirections hidden inside: |
| 1029 | // - colon-bound ParenExpressionAst args: -Name:('payload' > file) |
| 1030 | // - hashtable value statements: @{k='payload' > ~/.bashrc} |
| 1031 | // Both are invisible at the element level — the redirection's parent |
| 1032 | // is a child of CommandParameterAst / CommandExpressionAst, not a |
| 1033 | // separate pipeline element. Merge into statement-level redirections. |
| 1034 | // |
| 1035 | // The FindAll ALSO re-discovers direct-element redirections already |
| 1036 | // captured in the per-element loop above. Dedupe by (operator, target) |
| 1037 | // so tests and consumers see the real count. |
| 1038 | const seen = new Set(redirections.map(r => `${r.operator}\0${r.target}`)) |
| 1039 | for (const redir of ensureArray(raw.redirections)) { |
| 1040 | const r = transformRedirection(redir) |
| 1041 | const key = `${r.operator}\0${r.target}` |
| 1042 | if (!seen.has(key)) { |
| 1043 | seen.add(key) |
| 1044 | redirections.push(r) |
| 1045 | } |
| 1046 | } |
| 1047 | } else { |
| 1048 | // Non-pipeline statement: add synthetic command entry with full text |
| 1049 | commands.push({ |
| 1050 | name: raw.text, |
| 1051 | nameType: 'unknown', |
| 1052 | elementType: 'CommandExpressionAst', |
| 1053 | args: [], |
| 1054 | text: raw.text, |
| 1055 | }) |
| 1056 | // SECURITY: The PS1 else-branch does a direct recursive FindAll on |
| 1057 | // FileRedirectionAst to catch expression redirections inside control flow |
| 1058 | // (if/for/foreach/while/switch/try/trap/&& and ||). The CommandAst FindAll |
| 1059 | // above CANNOT see these: in if ($x) { 1 > /tmp/evil }, the literal 1 with |
nothing calls this directly
no test coverage detected