#!/usr/bin/env python3 """Fail-closed source regression checks. Not a replacement for running Rust tests.""" from pathlib import Path import re root = Path(__file__).resolve().parents[2] def read(name): return (root / name).read_text(encoding="utf-8") def must(source, *text): for item in text: if item not in source: raise AssertionError(f"missing security invariant: {item}") router = read('src/admin/router.rs') access = read('src/admin/access.rs') session = read('src/admin/session.rs') proxy = read('src/proxy/forward_entry.rs') maintenance = read('src/proxy/cache_http_invalidation.rs') operations = read('src/admin/operations.rs') cache_policy = read('src/cache/model/http_invalidation.rs') app = read('src/runtime/types.rs') + read('src/runtime/constructors.rs') must(router, 'fn anonymous_allowed', 'panel_document_path(path)', '/api/auth/status', '/api/auth/setup', '/api/auth/login', '.layer(middleware::from_fn_with_state(state.clone(), guard))', '"content-security-policy"', '"x-frame-options"', '"x-content-type-options"', '"no-referrer"', '"/api/system-info", get(system_info)', '"/api/events", get(events)', '"/api/cache/invalidate"', '"/api/config", get(configuration).merge(put(update))') must(session, 'if !public && session.is_none()', 'if unsafe_method && !external_certificate_api', 'single_header(headers, "origin")', 'single_header(headers, "x-rustproxy-request") != Some("1")', 'single_header(headers, "x-csrf-token")', 'auth::equal_secret', 'session_at_with_browser_lifetime', 'secure(next.run(request).await)', 'h == "cross-site"') must(access, 'admin_request_authority', 'admin_origin_matches', 'trusted_admin_proxy', 'single_header(headers, "x-forwarded-host")', 'certificate_api_credentials', 'valid_certificate_api_token') must(session, 'verify_certificate_api_credential') # Only the intended bootstrap/discovery APIs may appear inside the anonymous allowlist. allowlist = router.split('fn anonymous_allowed(', 1)[1].split('\n}', 1)[0] allowed_api_paths = set(re.findall(r'"(/api/[^" ]+)"', allowlist)) assert allowed_api_paths == {'/api/auth/status', '/api/auth/setup', '/api/auth/login'}, allowed_api_paths must(maintenance, 'permits_http_invalidation(client_ip)', 'snapshot.forwarding.trusted(peer_ip)', 'return error(StatusCode::FORBIDDEN, "RustProxy: missing trusted proxy client identity', 'cache_http_invalidation_request(request.method()', 'duplicate BAN/PURGE control header', 'request.body().is_end_stream()', 'get_all(http::header::CONTENT_LENGTH)', 'http::header::TRANSFER_ENCODING', 'X-RustProxy-Confirm-Wide: 1', '"{scheme}://{authority}{target}"', 'cache_invalidation_slots.clone().try_acquire_owned()', 'state.store.audit(', 'tokio::task::spawn_blocking', 'let _permit = permit;', 'StatusCode::UNPROCESSABLE_ENTITY', 'StatusCode::TOO_MANY_REQUESTS') # Axum 0.8 requires the `forward` fallback future to be Send. Its Body is # not Sync, so an async helper must own Request
rather than retain # &Request across the audit/cache await points. async_maintenance = maintenance.split('async fn cache_http_invalidation(', 1)[1].split(') -> Response {', 1)[0] assert 'request: Request,' in async_maintenance, 'cache maintenance must own Request in its async future' assert 'request: &Request' not in async_maintenance, 'borrowed Request makes fallback handler non-Send' must(maintenance, 'let method = request.method().clone();', 'drop(request);') assert maintenance.index('drop(request);') < maintenance.index('state.store.audit('), 'drop body before asynchronous audit' assert 'frontend, &host, request, context.client_ip' in proxy, 'forward must transfer request ownership' must(proxy, 'snapshot.host_allowed(&host, context.client_ip)', 'crate::acl::allows_with_country(', 'context.client_ip, peer.0.ip(), context.scheme', 'return cache_http_invalidation(') assert proxy.find('snapshot.host_allowed(&host, context.client_ip)') < proxy.find('return cache_http_invalidation(') assert proxy.find('if cache_http_invalidation_method(request.method())') < proxy.find('return cache_http_invalidation(') must(cache_policy, 'http_invalidation_allowed_cidrs.len() <= 32', 'net.prefix_len() > 0', 'pub fn permits_http_invalidation') must(app, 'cache_invalidation_slots: Arc