Внедрение KESL на ВМ2 + замена CLAMAV на KESL

This commit is contained in:
mi
2026-09-08 01:39:37 +03:00
parent 85df788f2d
commit fdfdeaffb4
43 changed files with 2210 additions and 329 deletions
@@ -1,5 +1,6 @@
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from uuid import uuid4
import httpx
@@ -7,6 +8,7 @@ import pytest
from app.api import create_app
from app.db import TaskStatus
from app.file_pipeline import AntivirusStatus
from app.repository import ConflictError
from app.service import SafetyService
from app.settings import EmergencyMode
@@ -54,6 +56,15 @@ class ForbiddenResolver:
raise AssertionError("MOCK must not call DNS")
class StaleAntivirus:
async def status(self):
return AntivirusStatus(
"12.4",
"sha256:" + "a" * 64,
datetime.now(UTC) - timedelta(hours=241),
)
def body(kind: str, message_id=None) -> dict:
value = {
"message_id": str(message_id or uuid4()),
@@ -188,3 +199,27 @@ async def test_final_task_response_keeps_task_config_snapshot(active_config) ->
assert final.status_code == 200
assert final.json()["config_version"] == 99
async def test_stale_kesl_databases_disable_only_files(active_config) -> None:
repo = FakeRepository()
service = SafetyService(
repo,
active_config,
EmergencyMode(),
ForbiddenResolver(),
antivirus=StaleAntivirus(),
)
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=create_app(service, "secret")),
base_url="http://test",
headers={"X-Service-Token": "secret"},
) as client:
file_response = await client.post(
"/internal/safety/v2/messages/check", json=body("file")
)
text_response = await client.post(
"/internal/safety/v2/messages/check", json=body("text")
)
assert file_response.status_code == 503
assert text_response.status_code == 200
@@ -35,12 +35,13 @@ def test_config_cross_field_and_manifest_subset(artifacts: Path) -> None:
validate_config(bad, artifacts)
def test_clamav_signature_age_policy_bounds(artifacts: Path) -> None:
def test_kesl_signature_age_policy_bounds(artifacts: Path) -> None:
document = seed(artifacts)
assert document["clamav"]["max_signature_age_hours"] == 240
document["clamav"]["max_signature_age_hours"] = 720
assert document["antivirus"]["engine"] == "kesl"
assert document["antivirus"]["max_signature_age_hours"] == 240
document["antivirus"]["max_signature_age_hours"] = 720
validate_config(document, artifacts)
document["clamav"]["max_signature_age_hours"] = 721
document["antivirus"]["max_signature_age_hours"] = 721
with pytest.raises(ValidationError):
validate_config(document, artifacts)
@@ -1,14 +1,24 @@
from __future__ import annotations
import asyncio
import hashlib
import io
import json
import struct
from pathlib import Path
from uuid import UUID
import pytest
from PIL import Image
from app.contracts import Attachment
from app.file_pipeline import ObjectChanged, collect_and_hash, detect_format
from app.file_pipeline import (
KeslSocketScanner,
ObjectChanged,
collect_and_hash,
detect_format,
one_chunk,
)
def image_bytes(format_name: str) -> bytes:
@@ -71,3 +81,65 @@ async def test_authoritative_stream_hash_and_size() -> None:
assert body == data and digest == hashlib.sha256(data).digest()
with pytest.raises(ObjectChanged):
await collect_and_hash(Reader(data), attachment(data, size=len(data) + 1), max_size=100)
class FakeWriter:
def __init__(self) -> None:
self.request = bytearray()
def write(self, value: bytes) -> None:
self.request.extend(value)
async def drain(self) -> None:
return None
def close(self) -> None:
return None
async def wait_closed(self) -> None:
return None
def framed(value: dict[str, object]):
body = json.dumps(value).encode()
reader = __import__("asyncio").StreamReader()
reader.feed_data(struct.pack(">I", len(body)) + body)
reader.feed_eof()
return reader
async def test_kesl_socket_clean_infected_and_status(monkeypatch, tmp_path: Path) -> None:
responses = [
{
"status": "completed",
"verdict": "clean",
"threat": None,
"engine_version": "12.4",
"signatures_version": "sha256:" + "a" * 64,
},
{
"status": "completed",
"verdict": "infected",
"threat": "EICAR-Test-File",
"engine_version": "12.4",
"signatures_version": "sha256:" + "b" * 64,
},
{
"status": "ready",
"engine_version": "12.4",
"databases_date": "2026-09-07T11:25:00+00:00",
"signatures_version": "sha256:" + "c" * 64,
},
]
async def connect(_):
return framed(responses.pop(0)), FakeWriter()
monkeypatch.setattr(asyncio, "open_unix_connection", connect, raising=False)
scanner = KeslSocketScanner(tmp_path / "scan.sock")
clean = await scanner.scan(one_chunk(b"clean"), scan_timeout=1)
infected = await scanner.scan(one_chunk(b"eicar"), scan_timeout=1)
status = await scanner.status()
assert clean.threat is None
assert infected.threat == "EICAR-Test-File"
assert status.engine_version == "12.4"