feat(security): add tirith pre-exec command scanning
Integrate tirith as a pre-execution security scanner that detects homograph URLs, pipe-to-interpreter patterns, terminal injection, zero-width Unicode, and environment variable manipulation — threats the existing 50-pattern dangerous command detector doesn't cover. Architecture: gather-then-decide — both tirith and the dangerous command detector run before any approval prompt, preventing gateway force=True replay from bypassing one check when only the other was shown to the user. New files: - tools/tirith_security.py: subprocess wrapper with auto-installer, mandatory cosign provenance verification, non-blocking background download, disk-persistent failure markers with retryable-cause tracking (cosign_missing auto-clears when cosign appears on PATH) - tests/tools/test_tirith_security.py: 62 tests covering exit code mapping, fail_open, cosign verification, background install, HERMES_HOME isolation, and failure recovery - tests/tools/test_command_guards.py: 21 integration tests for the combined guard orchestration Modified files: - tools/approval.py: add check_all_command_guards() orchestrator, add allow_permanent parameter to prompt_dangerous_approval() - tools/terminal_tool.py: replace _check_dangerous_command with consolidated check_all_command_guards - cli.py: update _approval_callback for allow_permanent kwarg, call ensure_installed() at startup - gateway/run.py: iterate pattern_keys list on replay approval, call ensure_installed() at startup - hermes_cli/config.py: add security config defaults, split commented sections for independent fallback - cli-config.yaml.example: document tirith security config
This commit is contained in:
parent
2fe853bcc9
commit
375ce8a881
9 changed files with 2153 additions and 1902 deletions
|
|
@ -132,6 +132,7 @@ def set_approval_callback(cb):
|
|||
from tools.approval import (
|
||||
detect_dangerous_command as _detect_dangerous_command,
|
||||
check_dangerous_command as _check_dangerous_command_impl,
|
||||
check_all_command_guards as _check_all_guards_impl,
|
||||
load_permanent_allowlist as _load_permanent_allowlist,
|
||||
DANGEROUS_PATTERNS,
|
||||
)
|
||||
|
|
@ -143,6 +144,12 @@ def _check_dangerous_command(command: str, env_type: str) -> dict:
|
|||
approval_callback=_approval_callback)
|
||||
|
||||
|
||||
def _check_all_guards(command: str, env_type: str) -> dict:
|
||||
"""Delegate to consolidated guard (tirith + dangerous cmd) with CLI callback."""
|
||||
return _check_all_guards_impl(command, env_type,
|
||||
approval_callback=_approval_callback)
|
||||
|
||||
|
||||
def _handle_sudo_failure(output: str, env_type: str) -> str:
|
||||
"""
|
||||
Check for sudo failure and add helpful message for messaging contexts.
|
||||
|
|
@ -951,10 +958,10 @@ def terminal_tool(
|
|||
env = new_env
|
||||
logger.info("%s environment ready for task %s", env_type, effective_task_id[:8])
|
||||
|
||||
# Check for dangerous commands (only for local/ssh in interactive modes)
|
||||
# Pre-exec security checks (tirith + dangerous command detection)
|
||||
# Skip check if force=True (user has confirmed they want to run it)
|
||||
if not force:
|
||||
approval = _check_dangerous_command(command, env_type)
|
||||
approval = _check_all_guards(command, env_type)
|
||||
if not approval["approved"]:
|
||||
# Check if this is an approval_required (gateway ask mode)
|
||||
if approval.get("status") == "approval_required":
|
||||
|
|
@ -964,13 +971,13 @@ def terminal_tool(
|
|||
"error": approval.get("message", "Waiting for user approval"),
|
||||
"status": "approval_required",
|
||||
"command": approval.get("command", command),
|
||||
"description": approval.get("description", "dangerous command"),
|
||||
"description": approval.get("description", "command flagged"),
|
||||
"pattern_key": approval.get("pattern_key", ""),
|
||||
}, ensure_ascii=False)
|
||||
# Command was blocked - include the pattern category so the caller knows why
|
||||
desc = approval.get("description", "potentially dangerous operation")
|
||||
# Command was blocked
|
||||
desc = approval.get("description", "command flagged")
|
||||
fallback_msg = (
|
||||
f"Command denied: matches '{desc}' pattern. "
|
||||
f"Command denied: {desc}. "
|
||||
"Use the approval prompt to allow it, or rephrase the command."
|
||||
)
|
||||
return json.dumps({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue