Skip to content

Commit 1fba72e

Browse files
author
sylviaanhe@tencent.com
committed
feature(Tool_Script_Safety_Guard): 工具脚本安全护栏功能
新增工具调用执行前的静态安全审查护栏,对 Python / Bash 脚本做模式扫描, 按策略给出 allow / review / deny 决策,并支持脱敏与审计落盘。 核心模块 (trpc_agent_sdk/tools/safety/): - engine.py 扫描引擎,编排扫描器并产出决策报告 - policy.py YAML 安全策略(白名单域名、允许命令、forbidden_paths、扫描限制) - rules.py/models.py 规则定义与数据模型 - scanners/ Python(AST+文本) 与 Bash 扫描器、共享 patterns 检测 - filter.py/wrapper.py 工具调用过滤器与包装器 - audit.py 审计日志(JSONL) 关键能力: - forbidden_paths 由策略驱动:带边界字面匹配 + ~ 展开,改策略即生效、无需改代码 - /dev /proc /sys 归类 FILE_OVERWRITE_DEVICE,其余 FILE_FORBIDDEN_PATH - 敏感信息脱敏 + max_line_length 截断防 ReDoS 示例 (examples/tool_safety_guard/): - run_scan.py / run_with_filter.py 端到端演示,含 12 个样例与 EXPECTED 基线 - 策略、报告、审计产物与 README(含已知限制) 测试 (tests/tools/safety/): - engine / policy / 扫描器 / 过滤器 / 验收 / forbidden_paths 配置驱动用例 - 12/12 样例决策与 EXPECTED 一致,deny 检出 6/6,安全样本误报 0 工具: - scripts/tool_safety_check.py CLI 入口
1 parent a0212cc commit 1fba72e

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Tests for policy-driven ``forbidden_paths`` enforcement (acceptance req. 6).
7+
8+
These prove the part of requirement 6 that says *forbidden paths* must be
9+
tunable from the policy file with **no code change**:
10+
11+
- a path listed in ``forbidden_paths`` is flagged and denied,
12+
- adding/removing a path in the policy flips the decision with no code change,
13+
- ``/dev`` / ``/proc`` / ``/sys`` map to ``FILE_OVERWRITE_DEVICE``,
14+
- boundary matching avoids false positives (``/etcd``, URL paths),
15+
- the overly-broad ``/`` entry is skipped.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from dataclasses import replace
21+
22+
from trpc_agent_sdk.tools.safety.engine import SafetyEngine
23+
from trpc_agent_sdk.tools.safety.models import Decision
24+
from trpc_agent_sdk.tools.safety.models import Language
25+
from trpc_agent_sdk.tools.safety.models import ScanInput
26+
from trpc_agent_sdk.tools.safety.policy import SafetyPolicy
27+
28+
29+
def _policy(forbidden_paths: list[str]) -> SafetyPolicy:
30+
"""A minimal policy that only configures the forbidden paths under test."""
31+
return replace(SafetyPolicy.default(), forbidden_paths=list(forbidden_paths))
32+
33+
34+
def _scan(policy: SafetyPolicy, script: str, language: Language = Language.PYTHON):
35+
return SafetyEngine(policy).scan(
36+
ScanInput(script=script, tool_name="t", language=language))
37+
38+
39+
def _rule_ids(report) -> set[str]:
40+
return {f.rule_id for f in report.findings}
41+
42+
43+
def test_forbidden_path_flagged_and_denied():
44+
script = 'open("/etc/cron.d/payload", "w").write("* * * * * root sh")\n'
45+
report = _scan(_policy(["/etc"]), script)
46+
assert "FILE_FORBIDDEN_PATH" in _rule_ids(report)
47+
assert report.decision == Decision.DENY
48+
49+
50+
def test_device_path_maps_to_overwrite_device():
51+
for forbidden, path in (("/dev", "/dev/sda"), ("/proc", "/proc/1/mem"), ("/sys", "/sys/power/state")):
52+
report = _scan(_policy([forbidden]), f'open("{path}", "wb")\n')
53+
assert "FILE_OVERWRITE_DEVICE" in _rule_ids(report), forbidden
54+
assert report.decision == Decision.DENY
55+
56+
57+
def test_config_driven_add_and_remove_no_code_change():
58+
"""The same script flips decision purely by editing the policy."""
59+
script = 'open("/data/prod/secret.txt", "w")\n'
60+
61+
# Path NOT in the policy -> nothing detected, allowed.
62+
before = _scan(_policy(["/etc"]), script)
63+
assert "FILE_FORBIDDEN_PATH" not in _rule_ids(before)
64+
assert before.decision == Decision.ALLOW
65+
66+
# Add the path to the policy -> now flagged and denied (no code change).
67+
after = _scan(_policy(["/etc", "/data/prod"]), script)
68+
assert "FILE_FORBIDDEN_PATH" in _rule_ids(after)
69+
assert after.decision == Decision.DENY
70+
71+
72+
def test_home_path_expanded():
73+
report = _scan(_policy(["~/.ssh"]), 'open("~/.ssh/config")\n')
74+
assert "FILE_FORBIDDEN_PATH" in _rule_ids(report)
75+
76+
77+
def test_no_false_positive_on_similar_prefix():
78+
"""A forbidden ``/etc`` must not match ``/etcd`` or a URL path component."""
79+
script = (
80+
'open("/etcd/data/state.db")\n'
81+
'url = "https://example.com/etc/info"\n'
82+
)
83+
report = _scan(_policy(["/etc"]), script)
84+
assert "FILE_FORBIDDEN_PATH" not in _rule_ids(report)
85+
86+
87+
def test_root_path_is_skipped_as_too_broad():
88+
"""A bare ``/`` entry is ignored (every path contains it)."""
89+
report = _scan(_policy(["/"]), 'open("/tmp/workdir/output.txt", "w")\n')
90+
assert "FILE_FORBIDDEN_PATH" not in _rule_ids(report)
91+
92+
93+
def test_bash_forbidden_path():
94+
report = _scan(_policy(["/etc"]), "cat /etc/shadow", language=Language.BASH)
95+
assert "FILE_FORBIDDEN_PATH" in _rule_ids(report)
96+
assert report.decision == Decision.DENY

0 commit comments

Comments
 (0)