3"""Shell operator masking for suppression inventory scans."""
5from __future__
import annotations
7from dataclasses
import dataclass, field
12 """One active shell lexical context."""
19class ShellOperatorState:
20 """Cross-line quote and command-substitution state."""
22 frames: list[ShellFrame] = field(default_factory=
lambda: [ShellFrame(
"normal")])
25def _mask_escape(code: str, index: int, masked: list[str]) -> int:
26 """Mask one escape and its following byte."""
28 if index + 1 < len(code):
34def _consume_single(code: str, index: int, state: ShellOperatorState, masked: list[str]) -> int:
35 """Consume one byte from a single-quoted literal."""
37 if code[index] ==
"'":
42def _consume_double(code: str, index: int, state: ShellOperatorState, masked: list[str]) -> int:
43 """Consume one byte from double quotes, exposing command substitutions."""
46 return _mask_escape(code, index, masked)
47 if code.startswith(
"$(", index):
49 state.frames.append(ShellFrame(
"command", 1))
57def _consume_active(code: str, index: int, state: ShellOperatorState, masked: list[str]) -> int:
58 """Consume one byte from ordinary or command-substitution shell syntax."""
60 frame = state.frames[-1]
62 return _mask_escape(code, index, masked)
65 state.frames.append(ShellFrame(
"single"))
68 state.frames.append(ShellFrame(
"double"))
71 if frame.kind ==
"backtick":
74 state.frames.append(ShellFrame(
"backtick"))
75 elif code.startswith(
"$(", index):
77 state.frames.append(ShellFrame(
"command", 1))
79 elif frame.kind ==
"command" and char ==
"(":
82 elif frame.kind ==
"command" and char ==
")":
92def mask_shell_operators(code: str, state: ShellOperatorState) -> str:
93 """Mask literal data but retain operators executed by this shell line."""
94 masked: list[str] = []
96 while index < len(code):
97 frame = state.frames[-1]
98 if frame.kind ==
"single":
99 index = _consume_single(code, index, state, masked)
100 elif frame.kind ==
"double":
101 index = _consume_double(code, index, state, masked)
103 index = _consume_active(code, index, state, masked)
104 return "".join(masked)