mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/tools: workspace root, path and command denylists
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_COMMAND_DENYLIST: frozenset[str] = frozenset(
|
||||
{
|
||||
"sudo",
|
||||
"su",
|
||||
"doas",
|
||||
"pkexec",
|
||||
"rm",
|
||||
"rmdir",
|
||||
"mkfs",
|
||||
"dd",
|
||||
"shutdown",
|
||||
"reboot",
|
||||
"poweroff",
|
||||
"halt",
|
||||
"useradd",
|
||||
"userdel",
|
||||
"passwd",
|
||||
"chmod",
|
||||
"chown",
|
||||
"mount",
|
||||
"umount",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceError(ValueError):
|
||||
"""Raised when a path or command violates workspace policy."""
|
||||
|
||||
|
||||
class _WorkspaceState:
|
||||
root: Path | None = None
|
||||
path_denylist: tuple[str, ...] = ()
|
||||
command_denylist: frozenset[str] = DEFAULT_COMMAND_DENYLIST
|
||||
|
||||
|
||||
_state = _WorkspaceState()
|
||||
|
||||
|
||||
def set_workspace_root(root: Path | str) -> Path:
|
||||
"""Set the workspace root used by tools; returns the resolved root."""
|
||||
path = Path(root).expanduser().resolve()
|
||||
if not path.is_dir():
|
||||
msg = f"workspace root is not a directory: {path}"
|
||||
raise WorkspaceError(msg)
|
||||
_state.root = path
|
||||
return path
|
||||
|
||||
|
||||
def get_workspace_root() -> Path:
|
||||
"""Return the configured workspace root."""
|
||||
if _state.root is None:
|
||||
msg = "workspace root is not set; call set_workspace_root() first"
|
||||
raise WorkspaceError(msg)
|
||||
return _state.root
|
||||
|
||||
|
||||
def clear_workspace_root() -> None:
|
||||
"""Clear workspace root (mainly for tests)."""
|
||||
_state.root = None
|
||||
|
||||
|
||||
def set_path_denylist(patterns: list[str] | tuple[str, ...] | None) -> None:
|
||||
"""Set path substring denylist (matched against resolved path strings)."""
|
||||
_state.path_denylist = tuple(patterns or ())
|
||||
|
||||
|
||||
def set_command_denylist(names: list[str] | tuple[str, ...] | frozenset[str] | None) -> None:
|
||||
"""Set denied command basenames (None restores defaults)."""
|
||||
_state.command_denylist = DEFAULT_COMMAND_DENYLIST if names is None else frozenset(names)
|
||||
|
||||
|
||||
def get_command_denylist() -> frozenset[str]:
|
||||
return _state.command_denylist
|
||||
|
||||
|
||||
def resolve_path(path: str | Path) -> Path:
|
||||
"""Resolve ``path`` under the workspace root; reject escapes and denylist hits."""
|
||||
root = get_workspace_root()
|
||||
candidate = Path(path)
|
||||
if not candidate.is_absolute():
|
||||
candidate = root / candidate
|
||||
resolved = candidate.expanduser().resolve()
|
||||
try:
|
||||
_ = resolved.relative_to(root)
|
||||
except ValueError as exc:
|
||||
msg = f"path escapes workspace root: {path}"
|
||||
raise WorkspaceError(msg) from exc
|
||||
resolved_str = str(resolved)
|
||||
for pattern in _state.path_denylist:
|
||||
if pattern and pattern in resolved_str:
|
||||
msg = f"path denied by policy: {path}"
|
||||
raise WorkspaceError(msg)
|
||||
return resolved
|
||||
|
||||
|
||||
def check_command_allowed(argv: list[str]) -> None:
|
||||
"""Raise if argv is empty or the executable basename is denylisted."""
|
||||
if not argv:
|
||||
msg = "command argv must not be empty"
|
||||
raise WorkspaceError(msg)
|
||||
binary = Path(argv[0]).name
|
||||
if binary in _state.command_denylist:
|
||||
msg = f"command denied by policy: {binary}"
|
||||
raise WorkspaceError(msg)
|
||||
Reference in New Issue
Block a user