|
6 | 6 |
|
7 | 7 | from __future__ import annotations |
8 | 8 |
|
| 9 | +import ast |
9 | 10 | import json |
10 | 11 | import time |
11 | 12 | from pathlib import Path |
12 | 13 | from unittest.mock import AsyncMock |
13 | 14 |
|
14 | 15 | import pytest |
15 | 16 |
|
| 17 | +from trpc_agent_sdk.code_executors import CodeBlock |
| 18 | +from trpc_agent_sdk.code_executors import CodeExecutionInput |
| 19 | +from trpc_agent_sdk.code_executors import BaseCodeExecutor |
| 20 | +from trpc_agent_sdk.code_executors import create_code_execution_result |
16 | 21 | from trpc_agent_sdk.context import create_agent_context |
17 | | -from trpc_agent_sdk.tools import FunctionTool |
| 22 | +from trpc_agent_sdk.filter import FilterResult |
18 | 23 | from trpc_agent_sdk.tools.safety import SafetyDecision |
19 | 24 | from trpc_agent_sdk.tools.safety import SafetyGuardedCodeExecutor |
20 | | -from trpc_agent_sdk.tools.safety import ToolSafetyBlockedError |
21 | | -from trpc_agent_sdk.tools.safety import ToolSafetyFinding |
22 | 25 | from trpc_agent_sdk.tools.safety import SafetyRiskLevel |
23 | 26 | from trpc_agent_sdk.tools.safety import ToolSafetyAuditLogger |
| 27 | +from trpc_agent_sdk.tools.safety import ToolSafetyBlockedError |
24 | 28 | from trpc_agent_sdk.tools.safety import ToolSafetyFilter |
25 | 29 | from trpc_agent_sdk.tools.safety import ToolSafetyGuard |
26 | 30 | from trpc_agent_sdk.tools.safety import ToolSafetyPolicy |
|
30 | 34 | from trpc_agent_sdk.tools.safety import apply_tool_safety_span_attributes |
31 | 35 | from trpc_agent_sdk.tools.safety import extract_script_from_tool_args |
32 | 36 | from trpc_agent_sdk.tools.safety import load_tool_safety_policy |
33 | | -from trpc_agent_sdk.code_executors import CodeBlock |
34 | | -from trpc_agent_sdk.code_executors import CodeExecutionInput |
35 | | -from trpc_agent_sdk.code_executors import BaseCodeExecutor |
36 | | -from trpc_agent_sdk.code_executors import create_code_execution_result |
37 | | -from trpc_agent_sdk.filter import FilterResult |
| 37 | +from trpc_agent_sdk.tools.safety import _scanner as scanner_module |
| 38 | +from trpc_agent_sdk.tools.safety import _types as types_module |
38 | 39 | from trpc_agent_sdk.tools.safety._guard import command_to_args |
39 | 40 | from trpc_agent_sdk.tools.safety._guard import infer_language_from_key |
40 | 41 | from trpc_agent_sdk.tools.safety._types import max_risk_level |
@@ -310,6 +311,71 @@ def test_scanner_command_arg_prefixes_and_duration_helpers(): |
310 | 311 | ] |
311 | 312 |
|
312 | 313 |
|
| 314 | +def test_scanner_edge_branches_from_direct_scans(monkeypatch): |
| 315 | + scanner = ToolSafetyScanner(policy(max_parallel_tasks=2, max_loop_iterations=3)) |
| 316 | + |
| 317 | + assert scanner.scan(ToolSafetyScanRequest(script="", language="python", env=[])).decision == SafetyDecision.ALLOW |
| 318 | + assert scanner._scan_bash_line("", 1) == [] # pylint: disable=protected-access |
| 319 | + |
| 320 | + monkeypatch.setattr(scanner_module, "split_shell_commands", lambda tokens: [[]]) |
| 321 | + assert scanner._scan_bash_line("echo ok", 1) == [] # pylint: disable=protected-access |
| 322 | + |
| 323 | + scripts = [ |
| 324 | + ("funcs = [print]\nfuncs[0]('x')", None), |
| 325 | + ("import os\nos.remove('.env')", "TSG-FILE-DENIED-PATH"), |
| 326 | + ("open('/etc/passwd', mode='w')", "TSG-FILE-SYSTEM-WRITE"), |
| 327 | + ("f = open('out.txt')", None), |
| 328 | + ("import concurrent.futures\nconcurrent.futures.ThreadPoolExecutor()", "TSG-RESOURCE-PARALLELISM"), |
| 329 | + ("for i in range(limit):\n pass", None), |
| 330 | + ("for i in range(2, 10, 2):\n pass", "TSG-RESOURCE-LARGE-LOOP"), |
| 331 | + ] |
| 332 | + for script, rule_id in scripts: |
| 333 | + report = scanner.scan(ToolSafetyScanRequest(script=script, language="python")) |
| 334 | + if rule_id: |
| 335 | + assert any(finding.rule_id == rule_id for finding in report.findings) |
| 336 | + |
| 337 | + |
| 338 | +def test_python_analyzer_helper_edge_branches(): |
| 339 | + analyzer = scanner_module._PythonAnalyzer("pass\n", policy()) # pylint: disable=protected-access |
| 340 | + |
| 341 | + assert analyzer.network_target_from_arg(ast.parse("url", mode="eval").body) == "" |
| 342 | + assert analyzer.node_line(ast.Pass(lineno=1, col_offset=0)) == "pass" |
| 343 | + assert analyzer.node_line(ast.Pass(lineno=99, col_offset=0)) == "" |
| 344 | + assert analyzer.call_name(ast.Attribute(value=ast.Constant(value=1), attr="field", ctx=ast.Load())) == "field" |
| 345 | + assert analyzer.call_name(ast.Constant(value=None)) == "" |
| 346 | + assert analyzer.name_of(ast.Attribute(value=ast.Name(id="obj", ctx=ast.Load()), attr="token", ctx=ast.Load())) == "token" |
| 347 | + assert analyzer.name_of(ast.Constant(value=None)) == "" |
| 348 | + assert analyzer.constant_string(None) == "" |
| 349 | + assert analyzer.constant_string(ast.parse("f'key={value}'", mode="eval").body) == "key={}" |
| 350 | + assert analyzer.constant_string(ast.parse("['a', value]", mode="eval").body) == "" |
| 351 | + assert analyzer.constant_string(ast.parse("('a', 'b')", mode="eval").body) == "a b" |
| 352 | + assert analyzer.path_literal_from_node(None) == "" |
| 353 | + assert analyzer.path_literal_from_node(ast.parse("Path('.env')", mode="eval").body) == ".env" |
| 354 | + assert analyzer.path_literal_from_node(ast.parse("Path(value)", mode="eval").body) == "" |
| 355 | + assert analyzer.path_literal_from_node(ast.parse("Path('.env').expanduser().read_text", mode="eval").body) == ".env" |
| 356 | + assert analyzer.path_literal_from_node(ast.parse("config.read_text", mode="eval").body) == "" |
| 357 | + assert analyzer.range_count(ast.parse("range(limit)", mode="eval").body) is None |
| 358 | + assert analyzer.range_count(ast.parse("range(2, 10, 2)", mode="eval").body) == 4 |
| 359 | + |
| 360 | + |
| 361 | +def test_scanner_module_helper_edge_branches(): |
| 362 | + assert scanner_module.is_secret_name("") is False |
| 363 | + assert scanner_module.numeric_constant(ast.parse("value", mode="eval").body) is None |
| 364 | + assert scanner_module.is_install_invocation(["python", "-m", "pip", "install", "demo"]) is False |
| 365 | + assert scanner_module.is_recursive_delete(["echo", "-rf"]) is False |
| 366 | + assert scanner_module.redirection_targets(["echo", "x", ">/etc/app.conf"]) == ["/etc/app.conf"] |
| 367 | + assert scanner_module.timeout_seconds_from_tokens(["timeout", "--kill-after=1s", "10m"]) == 600 |
| 368 | + assert scanner_module.timeout_seconds_from_tokens(["timeout", "--preserve-status", "10m"]) == 600 |
| 369 | + assert scanner_module.timeout_seconds_from_tokens(["timeout", "--preserve-status"]) is None |
| 370 | + assert scanner_module.duration_token_seconds("never") is None |
| 371 | + assert scanner_module.parallel_tasks_from_tokens(["parallel", "-j", "4"]) == 4 |
| 372 | + assert scanner_module.parallel_tasks_from_tokens(["parallel", "--jobs=7"]) == 7 |
| 373 | + assert scanner_module.parallel_tasks_from_tokens(["parallel", "echo"]) == 0 |
| 374 | + assert scanner_module.parallel_tasks_from_tokens(["xargs", "-P3"]) == 3 |
| 375 | + assert scanner_module.int_token("many") is None |
| 376 | + assert scanner_module.line_has_allowlisted_url("echo no-url", policy()) is False |
| 377 | + |
| 378 | + |
313 | 379 | def test_policy_loader_and_matching_branches(tmp_path): |
314 | 380 | policy_file = tmp_path / "policy.yaml" |
315 | 381 | policy_file.write_text( |
@@ -340,8 +406,11 @@ def test_policy_loader_and_matching_branches(tmp_path): |
340 | 406 | assert loaded.is_command_allowed("custom-tool --flag") is True |
341 | 407 | assert loaded.is_command_allowed(None) is False |
342 | 408 | assert loaded.is_denied_path(None) is False |
| 409 | + assert loaded.is_denied_path("config.yaml") is False |
343 | 410 | assert loaded.is_system_write_path("/") is True |
344 | 411 | assert loaded.is_system_write_path(None) is False |
| 412 | + assert ToolSafetyPolicy(allowed_domains=[""]).is_domain_allowed("api.example.com") is False |
| 413 | + assert ToolSafetyPolicy(system_write_paths=[""]).is_system_write_path("/etc/passwd") is False |
345 | 414 | assert load_tool_safety_policy(None).name == "default" |
346 | 415 |
|
347 | 416 |
|
@@ -370,6 +439,12 @@ def test_report_and_type_helpers_cover_empty_and_fallback_paths(): |
370 | 439 | assert json.loads(report.to_json(indent=None))["risk_count"] == 0 |
371 | 440 |
|
372 | 441 |
|
| 442 | +def test_max_risk_level_fallback_when_order_has_no_match(monkeypatch): |
| 443 | + monkeypatch.setattr(types_module, "RISK_LEVEL_ORDER", {}) |
| 444 | + |
| 445 | + assert types_module.max_risk_level(["unexpected"]) == SafetyRiskLevel.NONE |
| 446 | + |
| 447 | + |
373 | 448 | def test_audit_event_contains_required_fields(tmp_path): |
374 | 449 | report = ToolSafetyScanner(policy()).scan( |
375 | 450 | ToolSafetyScanRequest(script="rm -rf /tmp/demo", language="bash", tool_metadata={"name": "Bash"})) |
|
0 commit comments