test_lac_security.py python
83 lines 3.0 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """Security tests — Landing + access clarity (§LAC.12)."""
2
3 from __future__ import annotations
4
5 import re
6 from pathlib import Path
7
8 from tests.fixtures.app import seed_app_repo, start_test_app
9 from tools.app.server import STATIC_ROOT
10 from tools.landing.validate import SECRET_PATTERNS, validate_landing
11
12 KIT_ROOT = Path(__file__).resolve().parents[2]
13 LANDING = KIT_ROOT / "docs" / "landing"
14
15
16 def test_no_external_script_tags_on_landing() -> None:
17 for html_path in LANDING.rglob("*.html"):
18 text = html_path.read_text(encoding="utf-8")
19 assert not re.search(r"""<script[^>]+src\s*=\s*["']https?://""", text, re.I)
20 assert "eval(" not in text.lower()
21
22
23 def test_no_secret_heuristics_in_landing() -> None:
24 for html_path in LANDING.rglob("*.html"):
25 text = html_path.read_text(encoding="utf-8")
26 for pattern in SECRET_PATTERNS:
27 assert not pattern.search(text), f"{html_path} matched {pattern.pattern}"
28
29
30 def test_csrf_session_not_minted_as_values_in_html() -> None:
31 for path in (
32 LANDING / "index.html",
33 LANDING / "scenarios" / "index.html",
34 STATIC_ROOT / "index.html",
35 ):
36 text = path.read_text(encoding="utf-8")
37 # Must not embed process-lifetime token values (minted placeholders).
38 assert "session_credential=" not in text
39 assert re.search(r"session_credential\s*[:=]\s*['\"][A-Za-z0-9_-]{16,}", text) is None
40 assert re.search(r"csrf_token\s*[:=]\s*['\"][A-Za-z0-9_-]{16,}", text) is None
41
42
43 def test_repo_root_only_on_authenticated_health(tmp_path: Path) -> None:
44 seed_app_repo(tmp_path)
45 handle, client = start_test_app(tmp_path)
46 try:
47 # Authenticated health includes repo_root.
48 status, health = client.get("/api/health")
49 assert status == 200
50 assert "repo_root" in health["result"]
51
52 # Unauthenticated request is rejected (no anonymous leak of path).
53 import urllib.error
54 import urllib.request
55
56 req = urllib.request.Request(f"{client.base_url}/api/health")
57 try:
58 urllib.request.urlopen(req, timeout=5)
59 raise AssertionError("expected auth failure")
60 except urllib.error.HTTPError as exc:
61 body = exc.read().decode("utf-8")
62 assert exc.code in {401, 403}
63 assert str(tmp_path.resolve()) not in body
64 finally:
65 handle.shutdown()
66
67
68 def test_bind_auth_non_loopback_rules_unchanged() -> None:
69 """Non-loopback bind refusal remains fail-closed (Q0 closed surface)."""
70 from tools.app.bind import validate_bind_address
71 from tools.app.server import STATIC_ROOT
72
73 assert validate_bind_address("0.0.0.0") is None
74 assert validate_bind_address("::") is None
75 assert validate_bind_address("127.0.0.1") == "127.0.0.1"
76 js = (STATIC_ROOT / "assets" / "app.js").read_text(encoding="utf-8")
77 assert "localStorage" not in js
78 assert "auth-disable" not in js.lower()
79 assert "disableAuth" not in js
80
81
82 def test_validate_landing_security_green() -> None:
83 assert validate_landing(KIT_ROOT).ok
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago