fix(security): refresh client trust and publish validated SearXNG images

Repair SearXNG system and certifi stores, validate native amd64/arm64 runtime images through Compose, and publish the exact verified artifacts through a trusted CI job. Adopt released Python, certifi, grpcio, uv and Docker CLI trust fixes, with strict controls for all fourteen public incident chains.

Part of #6784 and #6790.

Co-authored-by: Léo <leograndcontact@gmail.com>
This commit is contained in:
RaresKeY
2026-10-23 22:32:33 +00:00
co-authored by Léo
parent f8e043e8ca
commit c6d76b9a9b
23 changed files with 1200 additions and 17 deletions
+106
View File
@@ -0,0 +1,106 @@
"""A network failure must not be mistaken for rejection of an incident chain."""
import asyncio
import importlib.util
import io
import json
from pathlib import Path
import sys
from types import ModuleType, SimpleNamespace
from unittest.mock import AsyncMock, patch
import pytest
ROOT = Path(__file__).resolve().parents[1]
INCIDENT = json.loads((ROOT / "tests/fixtures/ca-trust/incident.json").read_text())
@pytest.fixture
def validator():
repair = ModuleType("repair_trust")
repair.check_bundle = lambda path: None
spec = importlib.util.spec_from_file_location(
"searxng_validation", ROOT / "docker/searxng/validate_image.py"
)
module = importlib.util.module_from_spec(spec)
with patch.dict(sys.modules, {"repair_trust": repair}):
spec.loader.exec_module(module)
module.__file__ = str(ROOT / "tests/fixtures/ca-trust/validate_image.py")
return module
class RequestFailure(Exception):
def __init__(self, code):
super().__init__(f"curl error {code}")
self.code = code
@pytest.mark.parametrize("code", [60, 6, 28, 35, None])
def test_live_probe_requires_certificate_errors_for_every_host(validator, code):
hosts = []
async def get(url, **kwargs):
if url == "https://pypi.org/simple/certifi/":
return SimpleNamespace(status_code=200)
hosts.append(url.removeprefix("https://").removesuffix("/"))
assert kwargs["allow_redirects"] is False
if code is not None:
raise RequestFailure(code)
return SimpleNamespace(status_code=200)
client = SimpleNamespace(get=get, aclose=AsyncMock())
factory = ModuleType("searx.network.client")
def new_client(**kwargs):
assert kwargs["verify"] is True
return client
factory.new_client = new_client
errors = ModuleType("curl_cffi.requests.exceptions")
errors.RequestException = RequestFailure
modules = {"searx.network.client": factory, "curl_cffi.requests.exceptions": errors}
with patch.dict(sys.modules, modules), patch.object(sys, "path", list(sys.path)):
if code == 60:
asyncio.run(validator.check_https())
assert set(hosts) == set(INCIDENT["chains"])
else:
expected = "unexpectedly accepted" if code is None else "inconclusive"
with pytest.raises(RuntimeError, match=expected):
asyncio.run(validator.check_https())
client.aclose.assert_awaited_once()
def test_offline_probe_rejects_openssl_execution_failure(validator):
result = SimpleNamespace(returncode=1, stderr="openssl: unknown option", stdout="")
with patch.dict(validator.os.environ, {"SSL_CERT_FILE": "/unused.pem", "SSL_CERT_DIR": "/unused"}), \
patch.object(validator.subprocess, "run", return_value=result):
with pytest.raises(RuntimeError, match="not a certificate trust rejection"):
validator.check_chains()
@pytest.mark.parametrize("results", [[{"title": "ordinary result"}], []])
def test_running_probe_checks_existing_service_without_repair_or_second_boot(validator, tmp_path, results):
certificate = tmp_path / "01234567.0"
certificate.write_text("certificate double")
(tmp_path / "unrelated").write_text("ignored")
health = io.BytesIO(b"OK")
health.status = 200
search = io.BytesIO(json.dumps({"results": results}).encode())
with patch.dict(validator.os.environ, {"SSL_CERT_DIR": str(tmp_path), "GRANIAN_PORT": "8080"}), \
patch.object(validator.urllib.request, "build_opener") as opener, \
patch.object(validator, "check_chains") as chains, \
patch.object(validator, "check_bundle") as bundle, \
patch.object(validator, "check_https", new_callable=AsyncMock) as https, \
patch.object(validator.subprocess, "Popen") as boot:
opener.return_value.open.side_effect = [health, search]
if results:
validator.check_running(live=True)
else:
with pytest.raises(RuntimeError, match="no results"):
validator.check_running(live=True)
assert opener.return_value.open.call_args_list[0].args == ("http://127.0.0.1:8080/healthz",)
chains.assert_called_once_with()
bundle.assert_called_once_with(certificate)
https.assert_awaited_once()
boot.assert_not_called()