( command: string, parsed?: ParsedPowerShellCommand, )
| 1166 | * @returns true if the command is read-only, false otherwise |
| 1167 | */ |
| 1168 | export function isReadOnlyCommand( |
| 1169 | command: string, |
| 1170 | parsed?: ParsedPowerShellCommand, |
| 1171 | ): boolean { |
| 1172 | const trimmedCommand = command.trim() |
| 1173 | if (!trimmedCommand) { |
| 1174 | return false |
| 1175 | } |
| 1176 | |
| 1177 | // If no parsed AST available, conservatively return false |
| 1178 | if (!parsed) { |
| 1179 | return false |
| 1180 | } |
| 1181 | |
| 1182 | // If parsing failed, reject |
| 1183 | if (!parsed.valid) { |
| 1184 | return false |
| 1185 | } |
| 1186 | |
| 1187 | const security = deriveSecurityFlags(parsed) |
| 1188 | // Reject commands with script blocks — we can't verify the code inside them |
| 1189 | // e.g., Get-Process | ForEach-Object { Remove-Item C:\foo } looks like a safe pipeline |
| 1190 | // but the script block contains destructive code |
| 1191 | if ( |
| 1192 | security.hasScriptBlocks || |
| 1193 | security.hasSubExpressions || |
| 1194 | security.hasExpandableStrings || |
| 1195 | security.hasSplatting || |
| 1196 | security.hasMemberInvocations || |
| 1197 | security.hasAssignments || |
| 1198 | security.hasStopParsing |
| 1199 | ) { |
| 1200 | return false |
| 1201 | } |
| 1202 | |
| 1203 | const segments = getPipelineSegments(parsed) |
| 1204 | |
| 1205 | if (segments.length === 0) { |
| 1206 | return false |
| 1207 | } |
| 1208 | |
| 1209 | // SECURITY: Block compound commands that contain a cwd-changing cmdlet |
| 1210 | // (Set-Location/Push-Location/Pop-Location/New-PSDrive) alongside any other |
| 1211 | // statement. This was previously scoped to cd+git only, but that overlooked |
| 1212 | // the isReadOnlyCommand auto-allow path for cd+read compounds (finding #27): |
| 1213 | // Set-Location ~; Get-Content ./.ssh/id_rsa |
| 1214 | // Both cmdlets are in CMDLET_ALLOWLIST, so without this guard the compound |
| 1215 | // auto-allows. Path validation resolved ./.ssh/id_rsa against the STALE |
| 1216 | // validator cwd (e.g. /project), missing any Read(~/.ssh/**) deny rule. |
| 1217 | // At runtime PowerShell cd's to ~, reads ~/.ssh/id_rsa. |
| 1218 | // |
| 1219 | // Any compound containing a cwd-changing cmdlet cannot be auto-classified |
| 1220 | // read-only when other statements may use relative paths — those paths |
| 1221 | // resolve differently at runtime than at validation time. BashTool has the |
| 1222 | // equivalent guard via compoundCommandHasCd threading into path validation. |
| 1223 | const totalCommands = segments.reduce( |
| 1224 | (sum, seg) => sum + seg.commands.length, |
| 1225 | 0, |
no test coverage detected