test_lac_security.py
python
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