Solon v4.1.0

harness - BashToolStrategy 安全审计策略

</> markdown
2026年9月7日 上午11:26:07

当前 Bash 审计策略类是 BashToolStrategy,它实现 Agent 模块的 HITLStrategy

public class BashToolStrategy implements HITLStrategy {
    @Override
    public String evaluate(ReActTrace trace, Map<String, Object> args) {
        // 返回 null:放行
        // 返回非空字符串:进入 HITL,字符串作为审核原因
    }
}

Harness 默认把该策略绑定到 bash。但 HITL 默认关闭;要使它对 Harness Agent 生效,还需启用 hitlEnabled,并在 Agent 工具配置中包含 hitl

HarnessEngine engine = HarnessEngine.of("work", ".soloncode/")
        .sessionProvider(sessionProvider)
        .toolsAdd("bash", "hitl")
        .hitlEnabled(true)
        .build();

1、真实评估流程

BashToolStrategy.evaluate() 当前分两步:

  1. 按注册顺序直接检查内置 matcher;命中后立即返回放行或拦截结果。
  2. 未直接命中时,把权限上下文中的规则与内置规则交给 PermissionEngine 评估。

PermissionEngine 的排序规则是:

  • priority 越大越优先;
  • 同一优先级下,DENY > ALLOW > ASK
  • 工具名 * 匹配所有工具,其他工具名采用忽略大小写的精确匹配;
  • 参数规则可使用 glob pattern 或自定义 matcher。

需要特别注意默认值:

  • PermissionEngine.evaluate(toolName, args, context) 三参数重载未命中时默认返回 ASK
  • BashToolStrategy 调用的是带 defaultDecision 的四参数重载,而它自身的 defaultDecision 默认为 PermissionDecision.ALLOW

因此,当前 Bash 策略未命中任何规则时默认放行,不能把它描述为“所有非只读命令均要求人工确认”。如果需要默认询问,可显式配置:

BashToolStrategy bashStrategy = new BashToolStrategy()
        .defaultDecision(PermissionDecision.ASK);

2、内置 Bash 规则

源码中的内置规则如下,并非所谓“七层安全体系”。

2.1 空命令

command 为空时直接放行,即 evaluate() 返回 null。这与单独调用 BashCommandClassifier.classify("") 得到 DENY 的行为不同;BashToolStrategy 没有直接使用 classify() 的空命令分支。

2.2 字符串风险模式

命令字符串包含以下任一片段时拒绝:

  • 反引号 `
  • $(
  • /dev/

提示为“检测到潜在的命令注入或设备重定向风险。”。这是字符串包含判断,不是完整的 Shell 语法分析。

2.3 系统特权命令

当前正则只检查:

sudo, su, chown, chmod, chgrp, passwd, visudo

命中后返回包含原命令的“检测到系统特权指令”提示。当前源码没有把 killpkillsystemctlservice 等进程或服务命令加入此规则。

2.4 路径回溯

命令字符串包含 ../..\ 时拒绝。该规则是字面字符串检查,不是对规范化文件路径执行真实边界解析。

2.5 敏感路径

命令字符串包含以下任一片段时拒绝:

/etc/
/var/
/root/
~/.ssh/
~/.bashrc
~/.zshrc

同样,这些是字符串包含规则。

2.6 只读命令分类

BashCommandClassifier 维护只读/搜索命令集合,包括:

  • 搜索:grepegrepfgreprgagackfindfdlocate
  • 目录:lsdirtreeexa
  • 文件查看:catbatlessmoreheadtail
  • 信息与统计:wcfilewhichwhereiswhencetypestatdudf
  • 输出与文本处理:echoprintfdiffcommsortuniqcuttrawksed
  • 格式化:jqyqxmllint
  • 系统信息:pwdenvprintenvidwhoamihostnameunamedatecal
  • Git 只读双词命令:git loggit showgit diffgit statusgit branchgit taggit remotegit rev-parsegit ls-filesgit blamegit shortlog

这些命令仍先经过前述风险规则。例如 cat /etc/passwd 会先命中敏感路径规则,不能因为 cat 在只读集合中就放行。

只读分类还有两个例外:

  • sed -ised --in-place 等原地修改形式不算只读;
  • git branch -dgit branch -Dgit branch --delete 不算只读。

普通输出重定向 >>> 会使命令不再被归类为只读;当前实现对 2> 做了特殊排除,但这仍不是完整的 Shell 重定向解析。

2.7 管道与不完整命令

包含 | 时,分类器按管道分段;只有每一段都能被识别为只读命令,整个命令才按只读放行。这不是一份独立的“管道安全工具白名单”。

命令尾部是以下符号时,会按“不完整命令”拒绝:

|  &&  ||  ;

该检查只针对尾部,不代表完整拦截 ;& 或其他组合命令。

3、当前没有声明的内置硬拦截

不要把以下能力归因于当前 BashToolStrategy

  • 不存在包管理器或环境变更命令的统一硬拦截;
  • 不存在 curlwgetssh 等网络命令的统一硬拦截;
  • 不存在 rm -rfmv 的专门内置硬拦截;
  • 不存在完整的多命令组合解析与统一拒绝;
  • BashCommandClassifier 虽定义了 Git 写命令集合和 isGitWriteCommand(),但当前 BashToolStrategy 没有调用它。

这些命令如果没有命中其他内置规则或额外权限规则,会落到 Bash 策略的默认决策;当前默认是 ALLOW

4、通过 PermissionRule 添加约束

额外规则应通过 PermissionRule / PermissionContext 配置,而不是假定内置策略已经覆盖。

4.1 glob 规则

HarnessEngine engine = HarnessEngine.of("work", ".soloncode/")
        .sessionProvider(sessionProvider)
        .toolsAdd("bash", "hitl")
        .hitlEnabled(true)
        .build();

engine.addPermissionRule(
        PermissionRule.deny("bash", "rm -rf *").priority(10));
engine.addPermissionRule(
        PermissionRule.ask("bash", "git push *").priority(10));
engine.addPermissionRule(
        PermissionRule.allow("bash", "git log *").priority(10));

pattern 会针对参数中第一个可用字符串字段匹配,字段顺序为:

command, file_path, path, content, url, link

*? 按 glob 转换,且使用整串匹配;例如要匹配任意位置的关键词,需要在 pattern 中显式加入 *

PermissionRule.priority(int) 返回新规则,调用结果必须使用。对于自定义 matcher,应直接使用带 priority 参数的 PermissionRule.of(...) 创建;不要再调用 priority(...),因为当前实现会丢失 matcher 与 prompt。

4.2 自定义 matcher

PermissionRule denyNetwork = PermissionRule.of(
        "bash",
        PermissionBehavior.DENY,
        (toolName, args) -> {
            Object value = args == null ? null : args.get("command");
            if (!(value instanceof String)) {
                return false;
            }

            String command = ((String) value).trim();
            return command.startsWith("curl ")
                    || command.startsWith("wget ")
                    || command.startsWith("ssh ");
        },
        "网络命令被当前业务策略拒绝。",
        200);

engine.addPermissionRule(denyNetwork);

内置 matcher 在第一阶段直接命中时会立即返回。若业务要求完全不同的评估顺序或覆盖语义,更适合注册自定义 HITLStrategy,而不是依赖普通规则覆盖内置直接匹配。

5、自定义 HITLStrategy

HITLStrategy auditStrategy = new HITLStrategy() {
    @Override
    public String evaluate(ReActTrace trace, Map<String, Object> args) {
        Object value = args == null ? null : args.get("command");
        String command = value instanceof String ? (String) value : "";

        if (command.contains("rm -rf")) {
            return "检测到递归删除操作:" + command;
        }
        if (command.startsWith("curl ") || command.startsWith("wget ")) {
            return "网络访问需要人工确认:" + command;
        }
        return null;
    }
};

HITLInterceptor hitl = new HITLInterceptor()
        .onTool("bash", auditStrategy);

HarnessEngine engine = HarnessEngine.of("work", ".soloncode/")
        .sessionProvider(sessionProvider)
        .toolsAdd("bash", "hitl")
        .hitlInterceptor(hitl)
        .hitlEnabled(true)
        .build();

替换 HITLInterceptor 会同时替换 Harness 默认的 Bash 策略和默认 onApproved 记忆回调;需要的行为应自行重新注册。

6、运行时开关

engine.setHitlEnabled(true);
engine.setHitlEnabled(false);

该方法会同步更新 Harness 配置和拦截器的 enabled 状态。它只控制 HITL 是否启用,不会改变 BashToolStrategy 的规则集合或“未命中默认 ALLOW”的行为。