feat: RDG qwinsta/logoff via WinRM on client workstation (0.11.6)

SAC resolves internal_ip to a registered Windows host and runs qwinsta/logoff synchronously over WinRM instead of queueing commands to the gateway agent.
This commit is contained in:
2026-06-20 15:36:12 +10:00
parent 2eb06acb5b
commit 75f4c475df
13 changed files with 612 additions and 174 deletions
+27 -34
View File
@@ -15,11 +15,11 @@ from app.database import get_db
from app.models import Event, Host
from app.schemas.list_models import EventDetail, EventListResponse, EventSummary
from app.services.agent_commands import (
command_response_fields,
command_to_dict,
get_command_by_uuid,
queue_logoff,
queue_qwinsta,
)
from app.services.rdg_winrm_actions import execute_logoff_via_winrm, execute_qwinsta_via_winrm
from app.services.ingest import ingest_event
from app.services.event_summary import event_to_summary
from app.services.problems import maybe_create_problem
@@ -182,6 +182,26 @@ class AgentCommandResponse(BaseModel):
result_stderr: str | None = None
created_at: str | None = None
completed_at: str | None = None
target: str | None = None
client_hostname: str | None = None
internal_ip: str | None = None
def _agent_command_response(cmd) -> AgentCommandResponse:
data = command_to_dict(cmd)
extra = command_response_fields(cmd)
return AgentCommandResponse(
command_uuid=data["id"],
command_type=data["type"],
status=data["status"],
result_stdout=data.get("result_stdout"),
result_stderr=data.get("result_stderr"),
created_at=data.get("created_at"),
completed_at=data.get("completed_at"),
target=extra.get("target"),
client_hostname=extra.get("client_hostname"),
internal_ip=extra.get("internal_ip"),
)
class LogoffActionBody(BaseModel):
@@ -197,18 +217,9 @@ def post_event_qwinsta(
event = db.get(Event, event_db_id)
if event is None:
raise HTTPException(status_code=404, detail="Event not found")
cmd = queue_qwinsta(db, event, requested_by=str(user))
cmd = execute_qwinsta_via_winrm(db, event, requested_by=str(user))
db.commit()
data = command_to_dict(cmd)
return AgentCommandResponse(
command_uuid=data["id"],
command_type=data["type"],
status=data["status"],
result_stdout=data.get("result_stdout"),
result_stderr=data.get("result_stderr"),
created_at=data.get("created_at"),
completed_at=data.get("completed_at"),
)
return _agent_command_response(cmd)
@router.post("/{event_db_id}/actions/logoff", response_model=AgentCommandResponse)
@@ -221,23 +232,14 @@ def post_event_logoff(
event = db.get(Event, event_db_id)
if event is None:
raise HTTPException(status_code=404, detail="Event not found")
cmd = queue_logoff(
cmd = execute_logoff_via_winrm(
db,
event,
session_id=body.session_id,
requested_by=str(user),
)
db.commit()
data = command_to_dict(cmd)
return AgentCommandResponse(
command_uuid=data["id"],
command_type=data["type"],
status=data["status"],
result_stdout=data.get("result_stdout"),
result_stderr=data.get("result_stderr"),
created_at=data.get("created_at"),
completed_at=data.get("completed_at"),
)
return _agent_command_response(cmd)
@router.get("/{event_db_id}/actions/{command_uuid}", response_model=AgentCommandResponse)
@@ -250,16 +252,7 @@ def get_event_action_status(
cmd = get_command_by_uuid(db, command_uuid)
if cmd is None or cmd.event_id != event_db_id:
raise HTTPException(status_code=404, detail="Command not found")
data = command_to_dict(cmd)
return AgentCommandResponse(
command_uuid=data["id"],
command_type=data["type"],
status=data["status"],
result_stdout=data.get("result_stdout"),
result_stderr=data.get("result_stderr"),
created_at=data.get("created_at"),
completed_at=data.get("completed_at"),
)
return _agent_command_response(cmd)
@router.get("/{event_db_id}", response_model=EventDetail)