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 ()
0 commit comments