MCPcopy Index your code
hub / github.com/codeaashu/claude-code / transformCommandAst

Function transformCommandAst

src/utils/powershell/parser.ts:830–935  ·  view source on GitHub ↗
(
  raw: RawPipelineElement,
)

Source from the content-addressed store, hash-verified

828/** Transform a raw CommandAst pipeline element into ParsedCommandElement */
829// exported for testing
830export function transformCommandAst(
831 raw: RawPipelineElement,
832): ParsedCommandElement {
833 const cmdElements = ensureArray(raw.commandElements)
834 let name = ''
835 const args: string[] = []
836 const elementTypes: CommandElementType[] = []
837 const children: (CommandElementChild[] | undefined)[] = []
838 let hasChildren = false
839
840 // SECURITY: nameType MUST be computed from the raw name (before
841 // stripModulePrefix). classifyCommandName('scripts\\Get-Process') returns
842 // 'application' (contains \\) — the correct answer, since PowerShell resolves
843 // this as a file path. After stripping it becomes 'Get-Process' which
844 // classifies as 'cmdlet' — wrong, and allowlist checks would trust it.
845 // Auto-allow paths gate on nameType !== 'application' to catch this.
846 // name (stripped) is still used for deny-rule matching symmetry, which is
847 // fail-safe: deny rules over-match (Module\\Remove-Item still hits a
848 // Remove-Item deny), allow rules are separately gated by nameType.
849 let nameType: 'cmdlet' | 'application' | 'unknown' = 'unknown'
850 if (cmdElements.length > 0) {
851 const first = cmdElements[0]!
852 // SECURITY: only trust .value for string-literal element types with a
853 // string-typed value. Numeric ConstantExpressionAst (e.g. `& 1`) emits an
854 // integer .value that crashes stripModulePrefix() → parser falls through
855 // to passthrough. For non-string-literal or non-string .value, use .text.
856 const isFirstStringLiteral =
857 first.type === 'StringConstantExpressionAst' ||
858 first.type === 'ExpandableStringExpressionAst'
859 const rawNameUnstripped =
860 isFirstStringLiteral && typeof first.value === 'string'
861 ? first.value
862 : first.text
863 // SECURITY: strip surrounding quotes from the command name. When .value is
864 // unavailable (no StaticType on the raw node), .text preserves quotes —
865 // `& 'Invoke-Expression' 'x'` yields "'Invoke-Expression'". Stripping here
866 // at the source means every downstream reader of element.name (deny-rule
867 // matching, GIT_SAFETY_WRITE_CMDLETS lookup, resolveToCanonical, etc.)
868 // sees the bare cmdlet name. No-op when .value already stripped.
869 const rawName = rawNameUnstripped.replace(/^['"]|['"]$/g, '')
870 // SECURITY: PowerShell built-in cmdlet names are ASCII-only. Non-ASCII
871 // characters in cmdlet position are inherently suspicious — .NET
872 // OrdinalIgnoreCase folds U+017F (ſ) → S and U+0131 (ı) → I per
873 // UnicodeData.txt SimpleUppercaseMapping, so PowerShell resolves
874 // `ſtart-proceſſ` → Start-Process at runtime. JS .toLowerCase() does NOT
875 // fold these (ſ is already lowercase), so every downstream name
876 // comparison (NEVER_SUGGEST, deny-rule strEquals, resolveToCanonical,
877 // security validators) misses. Force 'application' to gate auto-allow
878 // (blocks at the nameType !== 'application' checks). Finding #31.
879 // Verified on Windows (pwsh 7.x, 2026-03): ſtart-proceſſ does NOT resolve.
880 // Retained as defense-in-depth against future .NET/PS behavior changes
881 // or module-provided command resolution hooks.
882 if (/[\u0080-\uFFFF]/.test(rawName)) {
883 nameType = 'application'
884 } else {
885 nameType = classifyCommandName(rawName)
886 }
887 name = stripModulePrefix(rawName)

Callers 1

transformStatementFunction · 0.85

Calls 5

ensureArrayFunction · 0.85
classifyCommandNameFunction · 0.85
stripModulePrefixFunction · 0.85
mapElementTypeFunction · 0.85
pushMethod · 0.45

Tested by

no test coverage detected