bash

package
v0.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 26, 2026 License: None detected not legal advice Imports: 0 Imported by: 0

Documentation

Overview

Bash AST 节点类型定义.

本文件定义了 Bash 命令解析后的抽象语法树(AST)节点类型. 用于安全分析,命令提取等下游任务.

设计原则:

  • 节点类型覆盖 Bash 核心语法(不追求完整性,够用即可)
  • 每个节点记录在源文本中的位置(Start/End)
  • 子节点按出现顺序排列
  • 宽容解析:无法识别的部分标记为 NodeWord

从 AST 提取安全相关信息.

本模块遍历 Bash AST,提取命令,参数,重定向等信息, 供权限系统和安全检查使用.

关键设计:

  • 递归遍历所有节点,包括子 shell,管道,列表中的命令
  • 跳过 env,sudo 等前缀,提取真正的命令名
  • 正确处理环境变量赋值前缀
  • 标记每个命令的上下文(是否在子 shell 中,管道位置等)

Heredoc 预处理和提取.

Heredoc 是 Bash 中跨多行的特殊语法,主解析器逐行处理时难以正确处理. 本模块在主解析之前预处理源文本:

  1. 扫描所有 heredoc 标记(<<EOF, <<-EOF, <<'EOF', <<"EOF")
  2. 提取 heredoc 体(从标记后的下一行开始,到匹配的分隔符结束)
  3. 将 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

View Source
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

func IsStaticRedirectTarget(target string) bool

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

type Assignment struct {
	Name  string // 变量名
	Value string // 变量值(原始文本,含引号)
}

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 行被移除.

算法:

  1. 逐行扫描,识别 << 标记
  2. 遇到换行时,如果有待处理的 heredoc,向下扫描 body
  3. 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

func Parse(source string) *Node

Parse 解析 Bash 命令字符串,返回 AST 根节点.

总是返回一个非 nil 的 NodeProgram 节点(宽容解析). 即使输入为空,也返回一个空的 Program 节点.

func (*Node) AddChild

func (n *Node) AddChild(child *Node)

AddChild 添加子节点.

func (*Node) CommandArgs

func (n *Node) CommandArgs() []string

CommandArgs 返回 SimpleCommand 节点的参数列表(不含命令名).

func (*Node) CommandName

func (n *Node) CommandName() string

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                             // 注释 # ...
)

func (NodeType) String

func (t NodeType) String() string

String 返回节点类型的可读名称.

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 的关键门控.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL