Module refinery.lib.scripts.ps1.deobfuscation.aliases

Inline command aliases defined via Set-Alias / New-Alias.

Expand source code Browse git
"""
Inline command aliases defined via Set-Alias / New-Alias.
"""
from __future__ import annotations

from refinery.lib.scripts import Node, Transformer
from refinery.lib.scripts.ps1.deobfuscation._helpers import (
    _case_normalize_name,
    _get_command_name,
    _string_value,
)
from refinery.lib.scripts.ps1.model import (
    Ps1CommandArgument,
    Ps1CommandArgumentKind,
    Ps1CommandInvocation,
    Ps1StringLiteral,
)

_ALIAS_COMMANDS = frozenset({'set-alias', 'sal', 'new-alias', 'nal'})


def _extract_alias_definition(cmd: Ps1CommandInvocation) -> tuple[str, str] | None:
    """
    Extract `(alias_name, target_command)` from a `Set-Alias` / `New-Alias`
    invocation. Handles:

    - Positional:  `sal aliasName targetCmd`
    - Named:       `Set-Alias -Name aliasName -Value targetCmd`
    - Mixed:       `Set-Alias aliasName -Value targetCmd`
    """
    alias_name: str | None = None
    target_name: str | None = None

    positional: list[str] = []
    for arg in cmd.arguments:
        if isinstance(arg, Ps1CommandArgument):
            if arg.kind == Ps1CommandArgumentKind.POSITIONAL:
                if arg.value is None:
                    return None
                sv = _string_value(arg.value)
                if sv is None:
                    return None
                positional.append(sv)
            elif arg.kind == Ps1CommandArgumentKind.NAMED:
                param = arg.name.lstrip('-').lower()
                if arg.value is None:
                    return None
                sv = _string_value(arg.value)
                if sv is None:
                    return None
                if param in ('name', 'n'):
                    alias_name = sv
                elif param in ('value', 'v', 'definition', 'd'):
                    target_name = sv
        else:
            sv = _string_value(arg)
            if sv is None:
                return None
            positional.append(sv)

    # Fill in from positional arguments
    if alias_name is None and len(positional) >= 1:
        alias_name = positional[0]
        positional = positional[1:]
    if target_name is None and len(positional) >= 1:
        target_name = positional[0]

    if alias_name is None or target_name is None:
        return None
    return alias_name, target_name


class Ps1AliasInlining(Transformer):
    """
    Replace command invocations that use aliases defined via Set-Alias / sal
    with their target command names.

    Alias definitions are intentionally kept in the AST: IEX inlining may
    parse new code in later iterations that references the same alias, so
    removing the definition prematurely would leave those references
    unresolved.
    """

    def visit(self, node: Node):
        aliases = self._collect_aliases(node)
        if not aliases:
            return None
        self._normalize_definitions(aliases)
        self._substitute(node, aliases)
        return None

    def _collect_aliases(self, root: Node) -> dict[str, tuple[Ps1CommandInvocation, str]]:
        """
        Collect alias definitions. Returns a mapping from `lower(alias_name)`
        to `(definition_cmd_node, target_command_name)`. Only aliases defined
        exactly once are included.
        """
        define_counts: dict[str, int] = {}
        definitions: dict[str, tuple[Ps1CommandInvocation, str]] = {}

        for node in root.walk():
            if not isinstance(node, Ps1CommandInvocation):
                continue
            name = _get_command_name(node)
            if name is None or name.lower() not in _ALIAS_COMMANDS:
                continue
            result = _extract_alias_definition(node)
            if result is None:
                continue
            alias_name, target_name = result
            key = alias_name.lower()
            define_counts[key] = define_counts.get(key, 0) + 1
            definitions[key] = (node, target_name)

        return {
            key: val for key, val in definitions.items()
            if define_counts.get(key, 0) == 1
        }

    def _normalize_definitions(
        self,
        aliases: dict[str, tuple[Ps1CommandInvocation, str]],
    ):
        """
        Normalize the target command name inside alias definition arguments.
        """
        for _key, (defn_node, target_name) in aliases.items():
            normalized = _case_normalize_name(target_name)
            if normalized == target_name:
                continue
            for arg in defn_node.arguments:
                literal = None
                if isinstance(arg, Ps1CommandArgument) and isinstance(arg.value, Ps1StringLiteral):
                    literal = arg.value
                elif isinstance(arg, Ps1StringLiteral):
                    literal = arg
                if literal is not None and literal.value.lower() == target_name.lower():
                    literal.value = normalized
                    literal.raw = normalized
                    self.mark_changed()
                    break

    def _substitute(
        self,
        root: Node,
        aliases: dict[str, tuple[Ps1CommandInvocation, str]],
    ):
        """
        Replace aliased command names with their targets.
        """
        for node in list(root.walk()):
            if not isinstance(node, Ps1CommandInvocation):
                continue
            name = _get_command_name(node)
            if name is None:
                continue
            key = name.lower()
            info = aliases.get(key)
            if info is None:
                continue
            defn_node, target_name = info
            if node is defn_node:
                continue
            normalized = _case_normalize_name(target_name)
            node.name = Ps1StringLiteral(
                offset=node.name.offset,
                value=normalized,
                raw=normalized,
            )
            node.name.parent = node
            self.mark_changed()

Classes

class Ps1AliasInlining

Replace command invocations that use aliases defined via Set-Alias / sal with their target command names.

Alias definitions are intentionally kept in the AST: IEX inlining may parse new code in later iterations that references the same alias, so removing the definition prematurely would leave those references unresolved.

Expand source code Browse git
class Ps1AliasInlining(Transformer):
    """
    Replace command invocations that use aliases defined via Set-Alias / sal
    with their target command names.

    Alias definitions are intentionally kept in the AST: IEX inlining may
    parse new code in later iterations that references the same alias, so
    removing the definition prematurely would leave those references
    unresolved.
    """

    def visit(self, node: Node):
        aliases = self._collect_aliases(node)
        if not aliases:
            return None
        self._normalize_definitions(aliases)
        self._substitute(node, aliases)
        return None

    def _collect_aliases(self, root: Node) -> dict[str, tuple[Ps1CommandInvocation, str]]:
        """
        Collect alias definitions. Returns a mapping from `lower(alias_name)`
        to `(definition_cmd_node, target_command_name)`. Only aliases defined
        exactly once are included.
        """
        define_counts: dict[str, int] = {}
        definitions: dict[str, tuple[Ps1CommandInvocation, str]] = {}

        for node in root.walk():
            if not isinstance(node, Ps1CommandInvocation):
                continue
            name = _get_command_name(node)
            if name is None or name.lower() not in _ALIAS_COMMANDS:
                continue
            result = _extract_alias_definition(node)
            if result is None:
                continue
            alias_name, target_name = result
            key = alias_name.lower()
            define_counts[key] = define_counts.get(key, 0) + 1
            definitions[key] = (node, target_name)

        return {
            key: val for key, val in definitions.items()
            if define_counts.get(key, 0) == 1
        }

    def _normalize_definitions(
        self,
        aliases: dict[str, tuple[Ps1CommandInvocation, str]],
    ):
        """
        Normalize the target command name inside alias definition arguments.
        """
        for _key, (defn_node, target_name) in aliases.items():
            normalized = _case_normalize_name(target_name)
            if normalized == target_name:
                continue
            for arg in defn_node.arguments:
                literal = None
                if isinstance(arg, Ps1CommandArgument) and isinstance(arg.value, Ps1StringLiteral):
                    literal = arg.value
                elif isinstance(arg, Ps1StringLiteral):
                    literal = arg
                if literal is not None and literal.value.lower() == target_name.lower():
                    literal.value = normalized
                    literal.raw = normalized
                    self.mark_changed()
                    break

    def _substitute(
        self,
        root: Node,
        aliases: dict[str, tuple[Ps1CommandInvocation, str]],
    ):
        """
        Replace aliased command names with their targets.
        """
        for node in list(root.walk()):
            if not isinstance(node, Ps1CommandInvocation):
                continue
            name = _get_command_name(node)
            if name is None:
                continue
            key = name.lower()
            info = aliases.get(key)
            if info is None:
                continue
            defn_node, target_name = info
            if node is defn_node:
                continue
            normalized = _case_normalize_name(target_name)
            node.name = Ps1StringLiteral(
                offset=node.name.offset,
                value=normalized,
                raw=normalized,
            )
            node.name.parent = node
            self.mark_changed()

Ancestors

Methods

def visit(self, node)
Expand source code Browse git
def visit(self, node: Node):
    aliases = self._collect_aliases(node)
    if not aliases:
        return None
    self._normalize_definitions(aliases)
    self._substitute(node, aliases)
    return None