diff --git a/backend/app/services/winrm_connect.py b/backend/app/services/winrm_connect.py index 0ab33e1..d903dd2 100644 --- a/backend/app/services/winrm_connect.py +++ b/backend/app/services/winrm_connect.py @@ -2,6 +2,7 @@ from __future__ import annotations +import base64 import socket from dataclasses import dataclass @@ -140,17 +141,37 @@ def run_winrm_logoff(*, target: str, user: str, password: str, session_id: int) ) +def _encode_powershell(script: str) -> str: + return base64.b64encode(script.encode("utf-16-le")).decode("ascii") + + +def _powershell_literal_path(path: str) -> str: + return path.replace("'", "''") + + def _winrm_rdp_update_remote_cmd(script_path: str) -> str: script = script_path.strip() if script: - return f'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{script}"' + literal = _powershell_literal_path(script) + ps = ( + f"$p = '{literal}'\n" + "if (-not (Test-Path -LiteralPath $p)) { throw 'Deploy script not found' }\n" + "& $p" + ) + else: + ps = ( + "$candidates = @(\n" + " (Join-Path $env:ProgramData 'LoginMonitor\\Deploy-LoginMonitor.ps1'),\n" + " (Join-Path $env:USERPROFILE 'Deploy-LoginMonitor.ps1')\n" + ")\n" + "$p = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1\n" + "if (-not $p) { throw 'Deploy-LoginMonitor.ps1 not found' }\n" + "& $p" + ) + encoded = _encode_powershell(ps) return ( - "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command " - '"& { $candidates = @(' - "(Join-Path $env:ProgramData 'LoginMonitor' 'Deploy-LoginMonitor.ps1')," - "(Join-Path $env:USERPROFILE 'Deploy-LoginMonitor.ps1')" - '); $p = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1; ' - "if (-not $p) { throw 'Deploy-LoginMonitor.ps1 not found' }; & $p }'" + "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass " + f"-EncodedCommand {encoded}" ) diff --git a/backend/app/version.py b/backend/app/version.py index 90b6d12..6e6cf6d 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.20.9" +APP_VERSION = "0.20.10" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/backend/tests/test_winrm_connect.py b/backend/tests/test_winrm_connect.py new file mode 100644 index 0000000..fcad69d --- /dev/null +++ b/backend/tests/test_winrm_connect.py @@ -0,0 +1,33 @@ +"""WinRM command builder tests.""" + +import base64 + +from app.services.winrm_connect import _winrm_rdp_update_remote_cmd + + +def test_rdp_update_default_uses_encoded_command_without_broken_quotes(): + cmd = _winrm_rdp_update_remote_cmd("") + assert "-EncodedCommand" in cmd + assert "-Command" not in cmd + assert '"& {' not in cmd + + encoded = cmd.rsplit(" ", 1)[-1] + script = base64.b64decode(encoded).decode("utf-16-le") + assert "Deploy-LoginMonitor.ps1" in script + assert "Join-Path $env:ProgramData" in script + + +def test_rdp_update_custom_script_uses_encoded_command(): + cmd = _winrm_rdp_update_remote_cmd(r"C:\ProgramData\LoginMonitor\Deploy-LoginMonitor.ps1") + assert "-EncodedCommand" in cmd + encoded = cmd.rsplit(" ", 1)[-1] + script = base64.b64decode(encoded).decode("utf-16-le") + assert r"C:\ProgramData\LoginMonitor\Deploy-LoginMonitor.ps1" in script + assert "Test-Path -LiteralPath" in script + + +def test_rdp_update_custom_script_escapes_single_quotes(): + cmd = _winrm_rdp_update_remote_cmd(r"C:\Users\O'Brien\Deploy-LoginMonitor.ps1") + encoded = cmd.rsplit(" ", 1)[-1] + script = base64.b64decode(encoded).decode("utf-16-le") + assert "O''Brien" in script diff --git a/frontend/src/version.ts b/frontend/src/version.ts index b1d57b0..b7a865e 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1,4 +1,4 @@ /** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */ export const APP_NAME = "Security Alert Center"; -export const APP_VERSION = "0.20.9"; +export const APP_VERSION = "0.20.10"; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;