Внедрение KESL на ВМ2 + замена CLAMAV на KESL
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user