fix: UTF-8 BOM in RDP bundle zip for Russian Windows PowerShell (0.20.16)
This commit is contained in:
@@ -10,6 +10,8 @@ from pathlib import Path
|
|||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
||||||
TTL_SECONDS = 600
|
TTL_SECONDS = 600
|
||||||
|
_UTF8_BOM = b"\xef\xbb\xbf"
|
||||||
|
_BOM_SUFFIXES = {".ps1", ".txt"}
|
||||||
|
|
||||||
_lock = Lock()
|
_lock = Lock()
|
||||||
_entries: dict[str, tuple[bytes, float]] = {}
|
_entries: dict[str, tuple[bytes, float]] = {}
|
||||||
@@ -21,13 +23,23 @@ def _prune_expired(now: float) -> None:
|
|||||||
_entries.pop(token, None)
|
_entries.pop(token, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _bundle_file_bytes(path: Path) -> bytes:
|
||||||
|
data = path.read_bytes()
|
||||||
|
if path.suffix.lower() in _BOM_SUFFIXES and not data.startswith(_UTF8_BOM):
|
||||||
|
return _UTF8_BOM + data
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def build_rdp_bundle_zip(repo_dir: Path, filenames: tuple[str, ...]) -> bytes:
|
def build_rdp_bundle_zip(repo_dir: Path, filenames: tuple[str, ...]) -> bytes:
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
|
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||||
for name in filenames:
|
for name in filenames:
|
||||||
path = repo_dir / name
|
path = repo_dir / name
|
||||||
if path.is_file():
|
if path.is_file():
|
||||||
archive.write(path, name)
|
info = zipfile.ZipInfo(name)
|
||||||
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
info.flag_bits |= 0x800
|
||||||
|
archive.writestr(info, _bundle_file_bytes(path))
|
||||||
return buffer.getvalue()
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ def _deploy_from_staging_body(staging_path: str) -> str:
|
|||||||
"$deploy = Join-Path $staging 'Deploy-LoginMonitor.ps1'\n"
|
"$deploy = Join-Path $staging 'Deploy-LoginMonitor.ps1'\n"
|
||||||
"if (-not (Test-Path -LiteralPath $deploy)) { throw \"Deploy-LoginMonitor.ps1 missing in staging\" }\n"
|
"if (-not (Test-Path -LiteralPath $deploy)) { throw \"Deploy-LoginMonitor.ps1 missing in staging\" }\n"
|
||||||
"Write-Output \"Running: $deploy -SourceShareRoot $staging\"\n"
|
"Write-Output \"Running: $deploy -SourceShareRoot $staging\"\n"
|
||||||
"& $deploy -SourceShareRoot $staging\n"
|
"powershell.exe -NoProfile -ExecutionPolicy Bypass -File $deploy -SourceShareRoot $staging\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -358,7 +358,8 @@ def _download_bundle_body(staging_path: str, bundle_url: str) -> str:
|
|||||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n"
|
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n"
|
||||||
"Write-Output \"Downloading bundle: $url\"\n"
|
"Write-Output \"Downloading bundle: $url\"\n"
|
||||||
"Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing\n"
|
"Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing\n"
|
||||||
"Expand-Archive -LiteralPath $zip -DestinationPath $staging -Force\n"
|
"Add-Type -AssemblyName System.IO.Compression.FileSystem\n"
|
||||||
|
"[System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $staging)\n"
|
||||||
"Remove-Item -LiteralPath $zip -Force\n"
|
"Remove-Item -LiteralPath $zip -Force\n"
|
||||||
"Write-Output \"Bundle extracted to $staging\"\n"
|
"Write-Output \"Bundle extracted to $staging\"\n"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
||||||
|
|
||||||
APP_NAME = "Security Alert Center"
|
APP_NAME = "Security Alert Center"
|
||||||
APP_VERSION = "0.20.15"
|
APP_VERSION = "0.20.16"
|
||||||
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
||||||
|
|||||||
@@ -3,7 +3,12 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from app.services.rdp_bundle_delivery import build_rdp_bundle_zip, get_rdp_bundle_zip, register_rdp_bundle_zip
|
from app.services.rdp_bundle_delivery import (
|
||||||
|
_UTF8_BOM,
|
||||||
|
build_rdp_bundle_zip,
|
||||||
|
get_rdp_bundle_zip,
|
||||||
|
register_rdp_bundle_zip,
|
||||||
|
)
|
||||||
from app.services.winrm_connect import (
|
from app.services.winrm_connect import (
|
||||||
RDP_BUNDLE_REQUIRED,
|
RDP_BUNDLE_REQUIRED,
|
||||||
RDP_REMOTE_STAGING,
|
RDP_REMOTE_STAGING,
|
||||||
@@ -30,8 +35,8 @@ def test_download_bundle_uses_invoke_webrequest():
|
|||||||
"https://sac.example/api/v1/agent/rdp-bundle/token",
|
"https://sac.example/api/v1/agent/rdp-bundle/token",
|
||||||
)
|
)
|
||||||
assert "Invoke-WebRequest" in script
|
assert "Invoke-WebRequest" in script
|
||||||
assert "Expand-Archive" in script
|
assert "ZipFile]::ExtractToDirectory" in script
|
||||||
assert "WriteAllBytes" not in script
|
assert "Expand-Archive" not in script
|
||||||
|
|
||||||
|
|
||||||
def test_deploy_from_staging_runs_local_bundle():
|
def test_deploy_from_staging_runs_local_bundle():
|
||||||
@@ -45,6 +50,19 @@ def test_custom_deploy_script_escapes_single_quotes():
|
|||||||
assert "O''Brien" in script
|
assert "O''Brien" in script
|
||||||
|
|
||||||
|
|
||||||
|
def test_rdp_bundle_zip_adds_utf8_bom_for_ps1(tmp_path: Path):
|
||||||
|
repo = tmp_path / "repo"
|
||||||
|
repo.mkdir()
|
||||||
|
(repo / "Deploy-LoginMonitor.ps1").write_text("# test", encoding="utf-8")
|
||||||
|
zip_bytes = build_rdp_bundle_zip(repo, ("Deploy-LoginMonitor.ps1",))
|
||||||
|
import zipfile
|
||||||
|
import io
|
||||||
|
|
||||||
|
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as archive:
|
||||||
|
data = archive.read("Deploy-LoginMonitor.ps1")
|
||||||
|
assert data.startswith(_UTF8_BOM)
|
||||||
|
|
||||||
|
|
||||||
def test_rdp_bundle_zip_roundtrip(tmp_path: Path):
|
def test_rdp_bundle_zip_roundtrip(tmp_path: Path):
|
||||||
repo = tmp_path / "repo"
|
repo = tmp_path / "repo"
|
||||||
repo.mkdir()
|
repo.mkdir()
|
||||||
@@ -69,7 +87,7 @@ def test_run_winrm_rdp_monitor_update_downloads_bundle_from_sac(tmp_path: Path):
|
|||||||
return WinRmCmdResult(ok=True, message="staging ok", target="pc", stdout="Staging ready")
|
return WinRmCmdResult(ok=True, message="staging ok", target="pc", stdout="Staging ready")
|
||||||
if "Invoke-WebRequest" in script:
|
if "Invoke-WebRequest" in script:
|
||||||
return WinRmCmdResult(ok=True, message="download ok", target="pc", stdout="Bundle extracted")
|
return WinRmCmdResult(ok=True, message="download ok", target="pc", stdout="Bundle extracted")
|
||||||
if "& $deploy" in script:
|
if "powershell.exe -NoProfile" in script or "& $deploy" in script:
|
||||||
return WinRmCmdResult(
|
return WinRmCmdResult(
|
||||||
ok=True,
|
ok=True,
|
||||||
message="WinRM OK (pc), exit 0",
|
message="WinRM OK (pc), exit 0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||||
export const APP_NAME = "Security Alert Center";
|
export const APP_NAME = "Security Alert Center";
|
||||||
export const APP_VERSION = "0.20.15";
|
export const APP_VERSION = "0.20.16";
|
||||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user