feat: host session list/terminate and event session actions (0.20.28)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,6 +20,13 @@ from app.services.agent_commands import (
|
||||
get_command_by_uuid,
|
||||
)
|
||||
from app.services.rdg_winrm_actions import execute_logoff_via_winrm, execute_qwinsta_via_winrm
|
||||
from app.services.host_sessions import (
|
||||
event_session_id,
|
||||
event_supports_session_terminate,
|
||||
terminate_session_for_event,
|
||||
)
|
||||
from app.services.linux_admin_settings import get_effective_linux_admin_config
|
||||
from app.services.win_admin_settings import get_effective_win_admin_config
|
||||
from app.services.ingest import ingest_event
|
||||
from app.services.event_summary import event_to_summary
|
||||
from app.services.event_type_visibility import get_hidden_event_types, visibility_type_filter
|
||||
@@ -214,6 +221,64 @@ class LogoffActionBody(BaseModel):
|
||||
session_id: int
|
||||
|
||||
|
||||
class EventSessionTerminateBody(BaseModel):
|
||||
session_id: str | None = None
|
||||
|
||||
|
||||
class EventSessionTerminateResponse(BaseModel):
|
||||
ok: bool
|
||||
message: str
|
||||
target: str | None = None
|
||||
stdout: str | None = None
|
||||
stderr: str | None = None
|
||||
|
||||
|
||||
@router.post("/{event_db_id}/actions/terminate-session", response_model=EventSessionTerminateResponse)
|
||||
def post_event_terminate_session(
|
||||
event_db_id: int,
|
||||
body: EventSessionTerminateBody | None = Body(default=None),
|
||||
db: Session = Depends(get_db),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> EventSessionTerminateResponse:
|
||||
event = db.scalar(
|
||||
select(Event).options(joinedload(Event.host)).where(Event.id == event_db_id)
|
||||
)
|
||||
if event is None:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
if not event_supports_session_terminate(event):
|
||||
raise HTTPException(status_code=400, detail="Event type does not support session terminate")
|
||||
|
||||
linux_cfg = get_effective_linux_admin_config(db)
|
||||
win_cfg = get_effective_win_admin_config(db)
|
||||
sid = (body.session_id if body else None) or event_session_id(event)
|
||||
|
||||
try:
|
||||
result = terminate_session_for_event(
|
||||
event,
|
||||
linux_cfg=linux_cfg,
|
||||
win_cfg=win_cfg,
|
||||
session_id=sid,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
if hasattr(result, "exit_code"):
|
||||
return EventSessionTerminateResponse(
|
||||
ok=result.ok,
|
||||
message=result.message,
|
||||
target=getattr(result, "target", None),
|
||||
stdout=getattr(result, "stdout", None) or None,
|
||||
stderr=getattr(result, "stderr", None) or None,
|
||||
)
|
||||
return EventSessionTerminateResponse(
|
||||
ok=result.ok,
|
||||
message=result.message,
|
||||
target=getattr(result, "target", None),
|
||||
stdout=getattr(result, "stdout", None) or None,
|
||||
stderr=getattr(result, "stderr", None) or None,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{event_db_id}/actions/qwinsta", response_model=AgentCommandResponse)
|
||||
def post_event_qwinsta(
|
||||
event_db_id: int,
|
||||
|
||||
@@ -29,6 +29,13 @@ from app.services.host_remote_actions import (
|
||||
)
|
||||
from app.services.agent_update_settings import get_effective_agent_update_config
|
||||
from app.services.agent_host_config import get_host_agent_settings, update_host_agent_config
|
||||
from app.services.host_sessions import (
|
||||
HostSessionRow,
|
||||
list_linux_sessions,
|
||||
list_windows_sessions,
|
||||
terminate_linux_session,
|
||||
terminate_windows_session,
|
||||
)
|
||||
from app.services.host_delete import delete_host_and_related
|
||||
from app.services.linux_admin_settings import get_effective_linux_admin_config
|
||||
from app.services.win_admin_settings import get_effective_win_admin_config
|
||||
@@ -302,6 +309,48 @@ class HostRemoteActionJobResponse(BaseModel):
|
||||
finished_at: str | None = None
|
||||
|
||||
|
||||
class HostSessionItem(BaseModel):
|
||||
session_id: str
|
||||
user: str
|
||||
tty: str | None = None
|
||||
state: str | None = None
|
||||
source_ip: str | None = None
|
||||
session_name: str | None = None
|
||||
|
||||
|
||||
class HostSessionsResponse(BaseModel):
|
||||
ok: bool
|
||||
message: str
|
||||
target: str | None = None
|
||||
sessions: list[HostSessionItem]
|
||||
|
||||
|
||||
class HostSessionTerminateBody(BaseModel):
|
||||
session_id: str
|
||||
|
||||
|
||||
class HostSessionTerminateResponse(BaseModel):
|
||||
ok: bool
|
||||
message: str
|
||||
target: str | None = None
|
||||
stdout: str | None = None
|
||||
stderr: str | None = None
|
||||
|
||||
|
||||
def _session_items(rows: list[HostSessionRow]) -> list[HostSessionItem]:
|
||||
return [
|
||||
HostSessionItem(
|
||||
session_id=r.session_id,
|
||||
user=r.user,
|
||||
tty=r.tty,
|
||||
state=r.state,
|
||||
source_ip=r.source_ip,
|
||||
session_name=r.session_name,
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def _ssh_action_response(
|
||||
result: SshCommandResult,
|
||||
*,
|
||||
@@ -550,6 +599,115 @@ def get_host_remote_job(
|
||||
return HostRemoteActionJobResponse(**get_remote_action_status(host))
|
||||
|
||||
|
||||
@router.post("/{host_id}/actions/sessions/list", response_model=HostSessionsResponse)
|
||||
def list_host_sessions(
|
||||
host_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_user=Depends(require_admin),
|
||||
) -> HostSessionsResponse:
|
||||
host = db.get(Host, host_id)
|
||||
if host is None:
|
||||
raise HTTPException(status_code=404, detail="Host not found")
|
||||
|
||||
from app.services.ssh_connect import is_linux_host
|
||||
from app.services.winrm_connect import is_windows_host
|
||||
|
||||
if is_linux_host(host):
|
||||
cfg = get_effective_linux_admin_config(db)
|
||||
if not cfg.configured:
|
||||
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
|
||||
try:
|
||||
sessions, result = list_linux_sessions(host, cfg)
|
||||
except SshHostNotLinuxError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except SshHostTargetMissingError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return HostSessionsResponse(
|
||||
ok=result.ok,
|
||||
message=result.message,
|
||||
target=result.target or None,
|
||||
sessions=_session_items(sessions),
|
||||
)
|
||||
|
||||
if is_windows_host(host):
|
||||
cfg = get_effective_win_admin_config(db)
|
||||
if not cfg.configured:
|
||||
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
|
||||
try:
|
||||
sessions, result = list_windows_sessions(host, cfg)
|
||||
except HostNotWindowsError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except HostTargetMissingError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
if result is None:
|
||||
raise HTTPException(status_code=502, detail="WinRM qwinsta failed")
|
||||
return HostSessionsResponse(
|
||||
ok=result.ok,
|
||||
message=result.message,
|
||||
target=result.target or None,
|
||||
sessions=_session_items(sessions),
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=400, detail="Host OS does not support live sessions")
|
||||
|
||||
|
||||
@router.post("/{host_id}/actions/sessions/terminate", response_model=HostSessionTerminateResponse)
|
||||
def terminate_host_session(
|
||||
host_id: int,
|
||||
body: HostSessionTerminateBody,
|
||||
db: Session = Depends(get_db),
|
||||
_user=Depends(require_admin),
|
||||
) -> HostSessionTerminateResponse:
|
||||
host = db.get(Host, host_id)
|
||||
if host is None:
|
||||
raise HTTPException(status_code=404, detail="Host not found")
|
||||
|
||||
from app.services.ssh_connect import is_linux_host
|
||||
from app.services.winrm_connect import is_windows_host
|
||||
|
||||
sid = body.session_id.strip()
|
||||
if not sid:
|
||||
raise HTTPException(status_code=422, detail="session_id is required")
|
||||
|
||||
if is_linux_host(host):
|
||||
cfg = get_effective_linux_admin_config(db)
|
||||
if not cfg.configured:
|
||||
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
|
||||
try:
|
||||
result = terminate_linux_session(host, cfg, sid)
|
||||
except SshHostNotLinuxError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except SshHostTargetMissingError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return HostSessionTerminateResponse(
|
||||
ok=result.ok,
|
||||
message=result.message,
|
||||
target=result.target or None,
|
||||
stdout=result.stdout or None,
|
||||
stderr=result.stderr or None,
|
||||
)
|
||||
|
||||
if is_windows_host(host):
|
||||
cfg = get_effective_win_admin_config(db)
|
||||
if not cfg.configured:
|
||||
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
|
||||
try:
|
||||
result = terminate_windows_session(host, cfg, sid)
|
||||
except HostNotWindowsError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
if result is None:
|
||||
raise HTTPException(status_code=502, detail="WinRM logoff failed")
|
||||
return HostSessionTerminateResponse(
|
||||
ok=result.ok,
|
||||
message=result.message,
|
||||
target=result.target or None,
|
||||
stdout=result.stdout or None,
|
||||
stderr=result.stderr or None,
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=400, detail="Host OS does not support session terminate")
|
||||
|
||||
|
||||
@router.patch("/{host_id}/agent-config", response_model=HostAgentConfigResponse)
|
||||
def patch_host_agent_config(
|
||||
host_id: int,
|
||||
|
||||
Reference in New Issue
Block a user