(args: string[])
| 1701 | } |
| 1702 | |
| 1703 | function isGhSafe(args: string[]): boolean { |
| 1704 | // gh commands are network-dependent; only allow for ant users |
| 1705 | if (process.env.USER_TYPE !== 'ant') { |
| 1706 | return false |
| 1707 | } |
| 1708 | |
| 1709 | if (args.length === 0) { |
| 1710 | return true |
| 1711 | } |
| 1712 | |
| 1713 | // Try two-word subcommand first (e.g. 'pr view') |
| 1714 | let config: ExternalCommandConfig | undefined |
| 1715 | let subcommandTokens = 0 |
| 1716 | |
| 1717 | if (args.length >= 2) { |
| 1718 | const twoWordKey = `gh ${args[0]?.toLowerCase()} ${args[1]?.toLowerCase()}` |
| 1719 | config = GH_READ_ONLY_COMMANDS[twoWordKey] |
| 1720 | subcommandTokens = 2 |
| 1721 | } |
| 1722 | |
| 1723 | // Try single-word subcommand (e.g. 'gh version') |
| 1724 | if (!config && args.length >= 1) { |
| 1725 | const oneWordKey = `gh ${args[0]?.toLowerCase()}` |
| 1726 | config = GH_READ_ONLY_COMMANDS[oneWordKey] |
| 1727 | subcommandTokens = 1 |
| 1728 | } |
| 1729 | |
| 1730 | if (!config) { |
| 1731 | return false |
| 1732 | } |
| 1733 | |
| 1734 | const flagArgs = args.slice(subcommandTokens) |
| 1735 | |
| 1736 | // SECURITY: Reject any arg containing `$` (variable reference). Bare |
| 1737 | // VariableExpressionAst positionals reach here as literal text ($env:SECRET). |
| 1738 | // deriveSecurityFlags does not gate bare Variable args — only subexpressions, |
| 1739 | // splatting, expandable strings, etc. All gh subcommands are network-facing, |
| 1740 | // so a variable arg is a data-exfiltration vector: |
| 1741 | // gh search repos $env:SECRET_API_KEY |
| 1742 | // → PowerShell expands at runtime → secret sent to GitHub API. |
| 1743 | // git ls-remote has an equivalent inline guard; this generalizes it for gh. |
| 1744 | // Bash equivalent: BashTool blanket `$` rejection at readOnlyValidation.ts:~1352. |
| 1745 | for (const arg of flagArgs) { |
| 1746 | if (arg.includes('$')) { |
| 1747 | return false |
| 1748 | } |
| 1749 | } |
| 1750 | if ( |
| 1751 | config.additionalCommandIsDangerousCallback && |
| 1752 | config.additionalCommandIsDangerousCallback('', flagArgs) |
| 1753 | ) { |
| 1754 | return false |
| 1755 | } |
| 1756 | return validateFlags(flagArgs, 0, config) |
| 1757 | } |
| 1758 | |
| 1759 | function isDockerSafe(args: string[]): boolean { |
| 1760 | if (args.length === 0) { |
no test coverage detected