feat(ne-exec): make max CLI commands configurable and allow traceroute

Add NETX_NE_EXEC_MAX_COMMANDS (default 5, cap 50) and whitelist traceroute/tracert/trace/trace6 prefixes for lab/ops use.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-07-28 10:57:55 +08:00
parent a6a5abc9ef
commit 88479245bb
9 changed files with 116 additions and 15 deletions

View file

@ -72,6 +72,8 @@ class Settings(BaseSettings):
ne_collect_pending_stale_sec: int = 180
ne_collect_run_timeout_cap_sec: int = 600
ne_collection_data_dir: str = "data/ne_collections"
# Managed NE exec: max CLI commands per request (lab can raise; hard-capped in ne_exec).
ne_exec_max_commands: int = 5
settings = Settings()

View file

@ -128,7 +128,7 @@ def api_delete_ume_synced_managed_ne(db: Session = Depends(get_db)):
@router.post("/exec")
def api_exec_managed_ne(body: ManagedNeExecRequest, db: Session = Depends(get_db)):
"""Login to a managed NE or UME inventory NE and run read-only CLI (show/display/ping)."""
"""Login to a managed NE or UME inventory NE and run read-only CLI (show/display/ping/traceroute)."""
return execute_managed_ne_commands(
db,
body.commands,

View file

@ -13,11 +13,16 @@ from .config import settings
from .ne_collect_runner import _collect_on_device
from .ne_crypto import credentials_configured
_EXEC_MAX_COMMANDS = 5
_EXEC_MAX_COMMANDS_CAP = 50
_EXEC_MAX_OUTPUT = 32_000
_EXEC_READ_TIMEOUT_DEFAULT = 60
_EXEC_READ_TIMEOUT_MAX = 120
def _exec_max_commands() -> int:
raw = int(settings.ne_exec_max_commands or 5)
return max(1, min(_EXEC_MAX_COMMANDS_CAP, raw))
# Block obvious config-change / destructive patterns (case-insensitive).
_BLOCKED_RE = re.compile(
r"(?i)("
@ -29,8 +34,10 @@ _BLOCKED_RE = re.compile(
r")"
)
# Read-only CLI: show/display plus ping reachability checks.
_ALLOWED_PREFIX_RE = re.compile(r"(?i)^(show\s|display\s|ping\s|ping6\s)")
# Read-only CLI: show/display plus ping/traceroute reachability checks.
_ALLOWED_PREFIX_RE = re.compile(
r"(?i)^(show\s|display\s|ping\s|ping6\s|traceroute\s|tracert\s|trace\s|trace6\s)"
)
# Unicode / C1 line separators that can smuggle a second CLI after a show prefix.
_FORBIDDEN_LINE_SEPARATORS = ("\u2028", "\u2029", "\x85", "\x0b", "\x0c")
@ -94,8 +101,9 @@ def execute_managed_ne_commands(
cmds = [str(c).strip() for c in commands if str(c).strip()]
if not cmds:
raise HTTPException(status_code=400, detail="commands_required")
if len(cmds) > _EXEC_MAX_COMMANDS:
raise HTTPException(status_code=400, detail=f"too_many_commands (max {_EXEC_MAX_COMMANDS})")
max_cmds = _exec_max_commands()
if len(cmds) > max_cmds:
raise HTTPException(status_code=400, detail=f"too_many_commands (max {max_cmds})")
for c in cmds:
_validate_command(c)

View file

@ -117,7 +117,8 @@ class ManagedNeExecRequest(BaseModel):
ne_id: str | None = None
ume_ne_id: str | None = None
commands: list[str] = Field(min_length=1, max_length=5)
# Absolute ceiling; runtime limit is settings.ne_exec_max_commands (default 5).
commands: list[str] = Field(min_length=1, max_length=50)
read_timeout_sec: int | None = Field(default=None, ge=10, le=120)