Documentation
¶
Overview ¶
Bash AST 节点类型定义.
本文件定义了 Bash 命令解析后的抽象语法树(AST)节点类型. 用于安全分析,命令提取等下游任务.
设计原则:
- 节点类型覆盖 Bash 核心语法(不追求完整性,够用即可)
- 每个节点记录在源文本中的位置(Start/End)
- 子节点按出现顺序排列
- 宽容解析:无法识别的部分标记为 NodeWord
从 AST 提取安全相关信息.
本模块遍历 Bash AST,提取命令,参数,重定向等信息, 供权限系统和安全检查使用.
关键设计:
- 递归遍历所有节点,包括子 shell,管道,列表中的命令
- 跳过 env,sudo 等前缀,提取真正的命令名
- 正确处理环境变量赋值前缀
- 标记每个命令的上下文(是否在子 shell 中,管道位置等)
Heredoc 预处理和提取.
Heredoc 是 Bash 中跨多行的特殊语法,主解析器逐行处理时难以正确处理. 本模块在主解析之前预处理源文本:
- 扫描所有 heredoc 标记(<<EOF, <<-EOF, <<'EOF', <<"EOF")
- 提取 heredoc 体(从标记后的下一行开始,到匹配的分隔符结束)
- 将 heredoc 体替换为空,让主解析器只需处理 << 操作符
关键细节:
- <<- 变体去除体内行首的 tab
- 引号包裹的分隔符阻止体内的变量展开
- 多个 heredoc 可以在同一行声明,体按声明顺序排列
Bash 递归下降解析器.
将 Bash 命令字符串解析为 AST(抽象语法树). 这是整个 Bash 安全分析的核心基础设施.
语法规则(简化版):
program → list*
list → pipeline (('&&' | '||' | ';' | '&') pipeline)*
pipeline → command ('|' command)*
command → simple_command | compound_command | function_def
simple_command → assignment* word+ redirection*
compound_command → '{' list '}' | '(' list ')' | if | for | while | case
redirection → op word | number? '>&' number
word → (unquoted | quoted | cmd_sub | var_expand | arith)+
设计原则:
- 宽容解析:遇到无法识别的语法,标记为 NodeWord 而不是报错
- 不追求完美:只需足够准确以支持安全分析
- 性能:1000 字符的命令在 1ms 内解析完
- 只使用 Go 标准库
Index ¶
- Constants
- func ExtractCommandName(cmd *CommandInfo) (command string, subcommand string)
- func GetCommandPrefixes(cmd *CommandInfo) []string
- func IsStaticRedirectTarget(target string) bool
- func ResolveHeredocBody(redir *RedirectionInfo) string
- type Assignment
- type CommandInfo
- type HeredocInfo
- type Node
- type NodeType
- type RedirectionInfo
Constants ¶
const MaxRecursionDepth = 20
MaxRecursionDepth 是 AST 遍历的最大递归深度.
精妙之处(CLEVER): 防止 $($($(nested))) 这种深度嵌套导致栈溢出. 超过深度限制的命令直接跳过提取,交给 AI 分类器或用户确认. 20 层足以覆盖任何正常脚本--实测 Linux kernel 的 configure 脚本最深也就 8 层.
Variables ¶
This section is empty.
Functions ¶
func ExtractCommandName ¶
func ExtractCommandName(cmd *CommandInfo) (command string, subcommand string)
ExtractCommandName 从命令中提取真正的命令名和子命令.
跳过以下前缀:
- env [VAR=value]* cmd → cmd
- sudo [-u user] cmd → cmd
- nohup cmd → cmd
- time cmd → cmd
- nice / ionice / strace 等 → cmd
- stdbuf 等 → cmd
返回 (command, subcommand). 对于 "git push --force",返回 ("git", "push").
func GetCommandPrefixes ¶
func GetCommandPrefixes(cmd *CommandInfo) []string
GetCommandPrefixes 获取命令的前缀列表,用于权限规则匹配.
对于命令 "npm install lodash",返回:
["npm", "npm install", "npm install lodash"]
func IsStaticRedirectTarget ¶
IsStaticRedirectTarget 检查重定向目标是否是静态的(无变量/命令替换).
升华改进(ELEVATED): 增强重定向目标的静态性检查,移植早期方案的安全细节. 替代方案:只检查 $ 和 `(简单版,覆盖 90% 场景但有安全盲点).
静态目标可以在编译时确定文件路径,用于安全规则匹配. 动态目标(含 $,`,* 等)可能在运行时展开为任意值. 精妙之处(CLEVER): 防御性检查链--逐一排除所有可能在运行时展开为意外值的字符. 这个函数看似简单,但遗漏任何一项都可能导致安全规则被绕过, 例如漏掉 ~ 检查就会让 `>~/.bashrc` 逃过路径权限检测.
func ResolveHeredocBody ¶
func ResolveHeredocBody(redir *RedirectionInfo) string
ResolveHeredocBody returns the runtime-effective heredoc body. When RedirectionInfo.HeredocStripTabs is true (the <<- form), bash strips leading tabs from every body line at runtime -- so a safety consumer matching literal paths or rerunning Parse on the body must look at the stripped content, not the raw one. For the plain << form the body is returned unchanged.
Returns "" for non-heredoc or nil input so callers can unconditionally call it without checking Operator first.
Cross-package consumers (pkg/permission/bash_security.go) use this to feed dangerous-file detection with the same string bash actually writes to a redirect target, closing the analyze-vs-runtime gap.
返回 heredoc body 的运行时有效内容. 当 RedirectionInfo.HeredocStripTabs 为 true (<<- 形式) 时, bash 运行时会去掉 body 每行的 leading tab, 安全消费者做字面路径匹配或对 body 再 Parse 必须看 strip 后的内容, 否则分析和运行时错位. 普通 << 形式 body 原样返回.
非 heredoc 或 nil 输入返回 "", 调用方无需先判断 Operator 即可调用.
跨包消费者 (pkg/permission/bash_security.go) 通过它喂 dangerous-file 检测, 让分析看到的 body 和 bash 写到重定向目标的一致, 消除分析与 运行时不一致的盲区.
Types ¶
type Assignment ¶
Assignment 表示一个变量赋值(VAR=value).
type CommandInfo ¶
type CommandInfo struct {
Name string // 命令名(已跳过 env/sudo 等前缀)
Args []string // 参数列表
Assignments []*Assignment // 环境变量前缀赋值
Redirections []*RedirectionInfo // 重定向列表
// Position is the sequential position within the current shell context.
// Semantics verified against extract.go: NodeSubshell starts a fresh
// context and resets Position to 0; NodeList / NodePipeline accumulate
// outer ctx.position + inner index (so a command inside a nested list
// sees outer parent position plus its own index). Position is NOT a
// global absolute index across the whole command string — use it to
// locate commands within a single shell level, not to uniquely
// identify a command in nested structures.
//
// Position 是当前 shell context 内的顺序位置. 语义按 extract.go 核实:
// NodeSubshell 开启新 context, Position 从 0 重算; NodeList /
// NodePipeline 内累加外层 ctx.position + 内部索引 (嵌套 list 里的
// 命令看到的是外层父位置 + 自己索引). Position 不是跨整条命令串的
// 全局绝对索引 — 用于在同一 shell 层级内定位命令, 不能唯一标识嵌套
// 结构中的某条命令.
Position int
// Operator is the bash connector preceding this command (&& / || / | / ;).
// The first command in a list / pipeline (i == 0) inherits the parent
// ctx.operator; an outermost command has "" (no preceding operator).
// Consumers: "" means "starting position with no prior connector";
// non-empty is one of &&, ||, |, ;.
//
// Operator 是命令前面的连接符 (&& / || / | / ;). list / pipeline 内第一个
// 命令 (i == 0) 从父 ctx.operator 继承, 最外层命令为 "" (无前序操作符).
// 消费者: "" 表示"起始位置, 无前序连接符"; 非空为 &&, ||, |, ; 之一.
Operator string
// InSubshell is true iff the command is enclosed in a NodeSubshell.
// Remains true inside nested subshells.
//
// InSubshell 在命令被 NodeSubshell 包裹时为 true. 嵌套 subshell 内仍为 true.
InSubshell bool
// InPipeline is true iff the command is a stage of a NodePipeline.
// **Always check this before reading PipePosition** — non-pipe commands
// have PipePosition == 0 (zero-value default), which collides with the
// "first pipeline stage" value. InPipeline is the only reliable
// discriminator.
//
// InPipeline 在命令为 NodePipeline 阶段时为 true. **读 PipePosition 前
// 必须先查 InPipeline** — 非管道命令 PipePosition == 0 (零值), 和"管道
// 第一阶段"的值冲突; InPipeline 是唯一可靠的区分.
InPipeline bool
// PipePosition is the stage index within a pipeline (0 = first stage).
// For non-pipe commands this field defaults to 0 which carries no
// meaning — always gate reads behind InPipeline.
//
// PipePosition 是管道内阶段位置 (0 = 第一阶段). 非管道命令时字段默认 0,
// 无实际含义 — 读取前先检查 InPipeline.
PipePosition int
Background bool // 位于 `&` 左侧, 后台执行. EN: left of an `&` connector, backgrounded.
RawText string // 命令的原始文本
// ArgQuoted is a parallel slice to Args (len(ArgQuoted) == len(Args))
// recording per-arg quoting regime at parse time. true means the arg
// was a fully-literal QuotedString (single quotes '...' or ANSI-C
// $'...') whose body undergoes no parameter / command substitution.
// false means the arg was either unquoted or double-quoted "...",
// where $VAR / $(cmd) expand at runtime. Mirrors the heredoc Quoted
// contract on RedirectionInfo so arg-level security analysis can
// distinguish "inert literal" args from "possibly expanded" args --
// a quoted `'$HOME/.ssh'` arg is *not* the same runtime target as
// an unquoted `$HOME/.ssh`, and downstream consumers (permission
// layer danger attribution) need the regime to render faithful
// reasons without re-parsing the original source.
//
// ArgQuoted 是与 Args 平行的 slice (len(ArgQuoted) == len(Args)),
// 记录每个 arg 在 parse 时的引号语义. true 表示该 arg 是完全字面
// QuotedString (单引号 '...' 或 ANSI-C $'...'), body 不做参数 /
// 命令替换. false 表示 arg 未加引号或在双引号 "..." 内, $VAR /
// $(cmd) 会在运行时展开. 与 RedirectionInfo 上 heredoc 的 Quoted
// 契约镜像, 让 arg 级安全分析区分 "inert literal" 与 "可能展开"
// -- `'$HOME/.ssh'` (quoted) 与 `$HOME/.ssh` (unquoted) 运行时
// 不是同一目标, 下游 (permission 层危险归因) 需要这个 regime 才
// 能给出忠实原因, 不必回到源文本二次解析.
ArgQuoted []bool
}
CommandInfo 表示提取出的一个命令的完整信息.
Background=true means this command was placed on the left of an `&` connector at some ancestor level -- bash runs it in the background and returns the prompt immediately, so the command escapes the foreground shell's lifecycle (cancellation, timeout, parent signal). Safety consumers treat this as a risk amplifier -- a backgrounded `rm -rf` cannot be cancelled by aborting the interactive session.
Background=true 表示该命令处于某层 `&` 连接符的左侧 -- bash 把它 放入后台并立即返回提示符, 命令即脱离前台 shell 的生命周期 (取消, 超时, 父进程信号). 安全消费者把它视为风险放大因子 -- 后台的 `rm -rf` 无法通过中止交互会话取消.
func ExtractAllCommands ¶
func ExtractAllCommands(root *Node) []*CommandInfo
ExtractAllCommands 从 AST 中递归提取所有命令,包括命令替换 $(...) 和反引号内的命令.
与 ExtractCommands 的区别:
- ExtractCommands 只提取顶层 AST 的命令,NodeCommandSubstitution 内部不递归
- ExtractAllCommands 对每个 NodeCommandSubstitution 节点,剥去 $(...) 外壳后 重新 Parse 内部文本,再递归提取其中的命令
精妙之处(CLEVER): 命令替换是安全检查最容易被绕过的地方-- `echo $(rm -rf /)` 中 rm -rf / 嵌套在命令替换里,ExtractCommands 只看到 echo, 安全检查会认为这条命令无害.ExtractAllCommands 递归展开所有嵌套层, 让危险命令无处遁形. 升华改进(ELEVATED): 反引号形式 `cmd` 和 $(...) 形式统一处理,覆盖全部命令替换语法. 替代方案:用正则匹配 $(...) 内容--否决:正则无法处理嵌套括号, 例如 $(echo $(rm -rf /)) 会导致括号匹配错误.
func ExtractCommands ¶
func ExtractCommands(root *Node) []*CommandInfo
ExtractCommands 从 AST 中提取所有简单命令.
递归遍历 AST 树,找到所有 SimpleCommand 节点,提取命令信息. 嵌套在子 shell,管道,列表中的命令都会被提取. 受 MaxRecursionDepth 限制,超深度的子树直接跳过.
注意:此函数不进入 NodeCommandSubstitution 节点内部. 若需要递归提取命令替换($(...))内的命令,使用 ExtractAllCommands.
type HeredocInfo ¶
type HeredocInfo struct {
Tag string // 分隔符标签(去除引号后)
Body string // heredoc 内容
BodyStart int // Body 在原文中的起始字节位置. EN: byte offset of Body start within the ORIGINAL source string.
BodyEnd int // Body 在原文中的结束字节位置 (半开区间). EN: byte offset of Body end (exclusive) within the ORIGINAL source string.
}
HeredocInfo is the preprocessor's per-heredoc output.
Tag/Body are consumed downstream (parser validates Tag, AST carries Body). BodyStart/BodyEnd are byte offsets into the ORIGINAL source string -- they let audit / security consumers locate where a heredoc body lives in the user's input, so "dangerous payload" warnings can point at "source bytes N-M" rather than a disembodied snippet. Invariant: source[BodyStart:BodyEnd] equals Body (modulo the synthetic trailing '\n' appended to non-empty bodies; see PreprocessHeredocs).
StripTabs / Quoted are deliberately NOT stored here -- the parser recomputes them from source tokens (op == "<<-", delimiter quoting), which is the authoritative source of truth. Duplicating them on this struct only created a parallel unread surface.
HeredocInfo 是预处理器对每个 heredoc 的输出.
Tag/Body 下游会消费 (parser 做 Tag 校验, AST 携带 Body). BodyStart/ BodyEnd 是 body 在**原文**中的字节偏移, 让审计 / 安全消费者能把 "危险载荷"警告定位到"源码字节 N-M", 而不是孤立的代码片段. 不变量: source[BodyStart:BodyEnd] 等于 Body (非空 body 末尾会补 一个合成 '\n', 详见 PreprocessHeredocs).
故意不存 StripTabs / Quoted -- parser 侧由源码 token (op == "<<-", 分隔符引号) 重新计算, 那才是 source of truth. 在本 struct 上重复 存只制造了一个无人读的平行字段.
func ExtractHeredocs ¶
func ExtractHeredocs(source string) []HeredocInfo
ExtractHeredocs 从源文本中提取所有 heredoc(不修改源文本).
func PreprocessHeredocs ¶
func PreprocessHeredocs(source string) (string, []HeredocInfo)
PreprocessHeredocs 预处理源文本中的所有 heredoc.
返回处理后的文本(heredoc 体被移除,只保留 << 操作符行)和提取的 heredoc 列表. 处理后的文本中,heredoc 的 << 操作符行保持不变,但其后的 body 行被移除.
算法:
- 逐行扫描,识别 << 标记
- 遇到换行时,如果有待处理的 heredoc,向下扫描 body
- body 从 << 行之后开始,直到遇到匹配的分隔符独占一行
type Node ¶
type Node struct {
Type NodeType // 节点类型
Value string // 节点的原始文本(或解析后的值)
Children []*Node // 子节点列表
Start int // 在源文本中的字节起始位置
End int // 字节结束位置(不含)
// Operator: List 节点的操作符(&&, ||, ;, &)
Operator string
// RedirectOp: Redirection/Heredoc 节点的操作符(>, >>, <, <<, 2>&1 等)
RedirectOp string
// RedirectTarget: 重定向目标文件名/fd
RedirectTarget string
// Quoted: QuotedString 节点 - true 表示单引号(阻止变量展开)
Quoted bool
// HeredocTag: Heredoc 节点的分隔符标签
HeredocTag string
// HeredocStripTabs: Heredoc <<- 变体,去除行首 tab
HeredocStripTabs bool
// HeredocQuoted: Heredoc 分隔符被引号包裹(阻止变量展开)
HeredocQuoted bool
// HeredocBody: Heredoc 的内容体
HeredocBody string
// HeredocBodyStart / HeredocBodyEnd: Heredoc body 在原文中的字节
// 半开区间 [start, end). 用于审计 / 安全消费者把危险 heredoc 定位
// 回用户输入的精确位置. 不变量: source[Start:End] 等于 HeredocBody
// (闭合 heredoc 末尾有合成 '\n', 未闭合没有).
//
// HeredocBodyStart / HeredocBodyEnd: byte offsets of the heredoc
// body within the original source, as a half-open interval
// [Start, End). Lets audit / security consumers pinpoint a dangerous
// heredoc back to the user's raw input. Invariant:
// source[Start:End] == HeredocBody (closed heredocs append a
// synthetic trailing '\n', unterminated ones do not).
HeredocBodyStart int
HeredocBodyEnd int
// Assignments: SimpleCommand 节点的环境变量赋值前缀(VAR=value cmd args)
Assignments []*Assignment
// Background: List 节点 - true 表示 & 后台执行
Background bool
}
Node 是 AST 中的一个节点.
所有 Bash 语法结构共用这一个节点类型,通过 Type 字段区分. 不同类型的节点使用不同的额外字段(未使用的字段保持零值).
func Parse ¶
Parse 解析 Bash 命令字符串,返回 AST 根节点.
总是返回一个非 nil 的 NodeProgram 节点(宽容解析). 即使输入为空,也返回一个空的 Program 节点.
func (*Node) CommandArgs ¶
CommandArgs 返回 SimpleCommand 节点的参数列表(不含命令名).
func (*Node) CommandName ¶
CommandName 返回 SimpleCommand 节点的命令名(第一个非赋值的 Word 子节点). 对于非 SimpleCommand 节点,返回空字符串.
type NodeType ¶
type NodeType int
NodeType 是 AST 节点类型枚举.
const ( NodeProgram NodeType = iota // 顶层程序(根节点) NodePipeline // 管道 cmd1 | cmd2 NodeList // 列表 cmd1 && cmd2 || cmd3 NodeSimpleCommand // 简单命令 ls -la NodeCompoundCommand // 复合命令 { cmd; } NodeSubshell // 子 shell (cmd) NodeIf // if/elif/else/fi NodeFor // for/do/done NodeWhile // while/do/done NodeCase // case/esac NodeFunction // function 定义 NodeRedirection // 重定向 > >> < 2>&1 等 NodeHeredoc // heredoc <<EOF ... EOF NodeAssignment // 变量赋值 VAR=value NodeWord // 单词(可能含引号,变量展开等) NodeCommandSubstitution // 命令替换 $(cmd) 或 `cmd` NodeProcessSubstitution // 进程替换 <(cmd) 或 >(cmd) NodeArithmeticExpansion // 算术展开 $((expr)) NodeVariableExpansion // 变量展开 $VAR 或 ${VAR} NodeQuotedString // 引号字符串 "..." 或 '...' 或 $'...' NodeComment // 注释 # ... )
type RedirectionInfo ¶
type RedirectionInfo struct {
Operator string // 操作符:>, >>, <, <<, 2>&1 等
Target string // 目标文件/fd
IsStatic bool // 目标是否静态(无变量展开)
HeredocTag string // Heredoc 分隔符标签 (仅 Operator 以 "<<" 开头时填值). EN: heredoc delimiter tag, populated only for "<<*" operators.
HeredocStripTabs bool // <<- 变体, 去行首 tab. EN: <<- variant, strip leading tabs from body.
HeredocQuoted bool // 分隔符被引号包裹, body 不展开变量. EN: delimiter was quoted; body does NOT expand variables / command substitutions.
HeredocBody string // Heredoc 原始 body. EN: raw heredoc body text.
// HeredocBodyStart / HeredocBodyEnd: Heredoc body 在**原文**的字节
// 半开区间, 仅 Operator 以 "<<" 开头且成功匹配到 body 时非零. 让
// AnalyzeDanger 等安全消费者把危险 heredoc 定位回用户源码位置,
// DangerInfo 可 surface "source bytes N-M" 给运维 / TUI / 审计.
//
// HeredocBodyStart / HeredocBodyEnd: byte offsets of the heredoc
// body within the ORIGINAL source (half-open interval). Populated
// only for "<<*" operators with a matched body. Lets security
// consumers (AnalyzeDanger) point DangerInfo back to concrete
// "source bytes N-M" for operators / TUI / audit.
HeredocBodyStart int
HeredocBodyEnd int
}
RedirectionInfo 表示一个重定向操作.
Heredoc fields (Tag/StripTabs/Quoted/Body) are populated only for NodeHeredoc redirections (Operator starting with "<<"). They surface the canonical heredoc semantics recorded by the parser so security consumers don't have to re-derive them from string shapes (e.g. consumers can read StripTabs directly rather than comparing Operator == "<<-"). Body is the raw heredoc content; Quoted=true means the delimiter was quoted (<<'EOF' / <<"EOF" / <<\EOF) so no parameter / command substitution expands inside Body -- a key gate for whether safety analysis needs to recursively parse Body.
Heredoc 字段 (Tag/StripTabs/Quoted/Body) 仅对 NodeHeredoc 重定向 (Operator 以 "<<" 开头) 填值, 把 parser 记录的 canonical heredoc 语义暴露给安全消费者, 避免它们再从字符串形状派生 (例如消费者直接 读 StripTabs 即可, 不必比较 Operator == "<<-"). Body 是 heredoc 原始内容; Quoted=true 表示分隔符被引号包裹 (<<'EOF' / <<"EOF" / <<\EOF), Body 内的 $VAR / $(cmd) 不会展开 -- 这是安全分析是否需 要递归解析 Body 的关键门控.