Skip to content

Commit fee73a2

Browse files
committed
fix: blocklist_commands enforcement, &-split, scanner cache, W605
- _scanner: blocklist_commands from policy now force DENY (was dead config) - _bash_scanner: & added to dispatch_commands separators - _scanner: get_scanner() cache invalidated on policy content_hash change - _bash_scanner: fix W605 invalid escape sequence in docstring
1 parent 6a4bdfa commit fee73a2

2 files changed

Lines changed: 28 additions & 5 deletions

File tree

trpc_agent_sdk/tools/safety/_bash_scanner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,14 +252,14 @@ def _dispatch_commands(self, line_no: int, raw_line: str, tokens: List[str]) ->
252252
"""
253253
seg_start = 0
254254
for i, t in enumerate(tokens):
255-
if t in (";", "&&", "||", "|"):
255+
if t in (";", "&&", "||", "|", "&"):
256256
self._analyse_one_command_with_dynamic_scan(line_no, raw_line, tokens[seg_start:i])
257257
seg_start = i + 1
258258
if seg_start < len(tokens):
259259
self._analyse_one_command_with_dynamic_scan(line_no, raw_line, tokens[seg_start:])
260260

261261
def _analyse_one_command_with_dynamic_scan(self, line_no: int, raw_line: str, cmd_tokens: List[str]) -> None:
262-
"""Analyse a command segment AND scan all tokens for eval/exec inside \$(...)."""
262+
r"""Analyse a command segment AND scan all tokens for eval/exec inside \$(...)."""
263263
self._analyse_one_command(line_no, raw_line, cmd_tokens)
264264
# Scan for dynamic commands that appear anywhere in the token stream
265265
# (not just as the head command), so that $(eval "rm -rf /") and

trpc_agent_sdk/tools/safety/_scanner.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,24 @@ def scan(self, scan_input: SafetyScanInput) -> SafetyScanReport:
225225
if decision != Decision.DENY:
226226
decision, bl_findings = self._check_blocklist_override(script, decision, scan_input.script_type)
227227
all_findings.extend(bl_findings)
228+
# Blocklist commands — also force DENY when a forbidden command literal appears
229+
if decision != Decision.DENY and self._policy.blocklist_commands:
230+
for cmd in self._policy.blocklist_commands:
231+
if re.search(re.escape(cmd), script, re.IGNORECASE):
232+
logger.warning("Blocklist command matched: %s → forcing DENY", cmd)
233+
all_findings.append(
234+
SafetyFinding(
235+
rule_id="FILE-001",
236+
category=RiskCategory.DANGEROUS_FILE_OPS,
237+
risk_level=RiskLevel.CRITICAL,
238+
evidence=cmd,
239+
message=f"Blocklisted command detected: {cmd}",
240+
recommendation="Remove the dangerous command from the script.",
241+
line_number=0,
242+
matched_pattern=cmd,
243+
))
244+
decision = Decision.DENY
245+
break
228246

229247
# Apply allow-pattern override — allow patterns → allow
230248
# Only upgrades NEEDS_HUMAN_REVIEW; never overrides DENY (blocklist wins).
@@ -857,10 +875,15 @@ def _redact_evidence(self, findings: list[SafetyFinding]) -> list[SafetyFinding]
857875

858876

859877
def get_scanner() -> SafetyScanner:
860-
"""Return (and cache) the default SafetyScanner instance."""
878+
"""Return (and cache) the default SafetyScanner instance.
879+
880+
The cache is invalidated on policy change so that ``reload_policy()``
881+
+ ``quick_scan()`` sees the updated rules.
882+
"""
861883
global _default_scanner # pylint: disable=global-statement
862-
if _default_scanner is None:
863-
_default_scanner = SafetyScanner()
884+
current_policy = get_policy()
885+
if _default_scanner is None or _default_scanner._policy.content_hash != current_policy.content_hash:
886+
_default_scanner = SafetyScanner(policy=current_policy)
864887
return _default_scanner
865888

866889

0 commit comments

Comments
 (0)