diff --git a/scripts/kb/gen_pages.py b/scripts/kb/gen_pages.py index b18658f..d5c552f 100644 --- a/scripts/kb/gen_pages.py +++ b/scripts/kb/gen_pages.py @@ -25,6 +25,13 @@ Wyjście: build/kb-site/ Stopka każdej strony: data generacji + krótki hash commita (`git rev-parse --short HEAD`). +Wystawka stoi za bramką NPM na `?key=`, więc każdy link wewnętrzny musi +nieść ten token — inaczej kliknięcie ze spisu wpada w 403 i działa wyłącznie +ręcznie sklejony URL. Token podaje się przy generacji (`--access-token` albo +zmienna `ACCESS_TOKEN`) i NIGDY nie trafia do repo: to parametr runtime, nie +stała w kodzie. Bez tokenu strony generują się jak dotąd, z gołymi linkami — +to jest tryb lokalnego podglądu, nie błąd. + Tryb `--check` nie generuje niczego — skanuje JUŻ WYGENEROWANY katalog wyjściowy w poszukiwaniu wycieków (adresy IP, porty, ścieżki hosta, tokeny). Świadome wyjątki trzymamy w scripts/kb/check_whitelist.txt. Trafienie = exit 1. @@ -35,7 +42,7 @@ frontmattera jest współdzielony z check_okf.py, żeby obie ścieżki widziały frontmatter dokładnie tak samo. Uruchomienie: - python3 scripts/kb/gen_pages.py [--base-url URL] [--out KATALOG] + python3 scripts/kb/gen_pages.py [--base-url URL] [--out KATALOG] [--access-token TOKEN] python3 scripts/kb/gen_pages.py --check [--out KATALOG] [--whitelist PLIK] """ @@ -43,11 +50,13 @@ from __future__ import annotations import argparse import html +import os import posixpath import re import shutil import subprocess import sys +import urllib.parse from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path @@ -87,6 +96,43 @@ TYPE_ORDER = [ ] +# --- token dostępu ---------------------------------------------------- + +ACCESS_TOKEN_ENV = "ACCESS_TOKEN" +ACCESS_TOKEN_PARAM = "key" + +# Token bramki NPM. Ustawiany raz na starcie build(), czytany przez with_token(). +# Modułowy, bo wewnętrzne href-y powstają w trzech miejscach na dnie rekurencji +# renderera (render_inline → render_blocks → render_blocks …); przewlekanie go +# parametrem przez cały renderer zaśmieciłoby każdą sygnaturę po drodze. +# Wartość NIGDY nie jest zapisywana w repo — pochodzi z --access-token/ACCESS_TOKEN. +_ACCESS_TOKEN = "" + + +def set_access_token(token: str) -> None: + global _ACCESS_TOKEN + _ACCESS_TOKEN = token or "" + + +def with_token(href: str) -> str: + """Dopisuje `?key=` do wewnętrznego odnośnika. + + Bez ustawionego tokenu zwraca href bez zmian — generacja lokalna do podglądu + ma dawać dokładnie to co dotąd. Fragment (`#sekcja`) zostaje na końcu, bo + query string idzie PRZED kotwicą. Gdyby URL kiedyś niósł własne parametry, + doklejamy `&` zamiast `?` — dziś nie niesie, ale to jeden warunek. + """ + if not _ACCESS_TOKEN: + return href + base, hash_sep, fragment = href.partition("#") + if not base: + # Czysta kotwica w obrębie strony — nie ma czego bramkować. + return href + sep = "&" if "?" in base else "?" + token = urllib.parse.quote(_ACCESS_TOKEN, safe="") + return f"{base}{sep}{ACCESS_TOKEN_PARAM}={token}{hash_sep}{fragment}" + + # --- model ------------------------------------------------------------ @@ -242,6 +288,9 @@ def render_inline(text: str) -> str: attrs = "" if href.startswith(("http://", "https://", "mailto:")): attrs = ' target="_blank" rel="noopener" class="ext"' + else: + # Wewnętrzny (strona .html albo kotwica) — musi nieść token bramki. + href = with_token(href) return f'{label}' text = _LINK_RE.sub(link, text) @@ -457,7 +506,8 @@ def page_shell( ) -> str: up = "../" * depth if nav is None: - nav = f'← All documents' + back = html.escape(with_token(f"{up}index.html"), quote=True) + nav = f'← All documents' nav_html = f"\n" if nav else "" return f""" @@ -546,7 +596,7 @@ def render_index(docs: list[Doc], base_url: str, stamp: str) -> str: for doc in items: lead = summary(doc) cards.append( - f'
  • ' + f'
  • ' f"{html.escape(doc.title)}" f'{html.escape(doc.doc_type)}' + (f"

    {html.escape(lead)}

    " if lead else "") @@ -589,7 +639,8 @@ def git_commit() -> str: return "unknown" -def build(out_dir: Path, base_url: str) -> list[Doc]: +def build(out_dir: Path, base_url: str, access_token: str = "") -> list[Doc]: + set_access_token(access_token) docs = load_docs() public = [d for d in docs if d.public] published = {d.id: d.page for d in public} @@ -618,6 +669,11 @@ def build(out_dir: Path, base_url: str) -> list[Doc]: print(f"Źródło: {KB_DIR.relative_to(REPO_ROOT)}/**/*.md") print(f"Wyjście: {out_dir}") print(f"BASE_URL: {base_url}") + # Sam token nigdy nie leci na stdout — logi z generacji bywają wklejane. + print( + f"Token: {'TAK' if _ACCESS_TOKEN else 'NIE'} " + f"(linki wewnętrzne {'z' if _ACCESS_TOKEN else 'bez'} ?{ACCESS_TOKEN_PARAM}=…)" + ) print( f"Dokumenty: {len(docs)} razem, {len(public)} public, " f"{len(docs) - len(public)} pominiętych (private / brak frontmattera)" @@ -671,6 +727,9 @@ _NEUTRAL_IPV6 = {"::", "::1"} # tylko `::` z adresów IPv6; te i tak raportuje wzorzec `ip-v6`. _PORT_RE = re.compile(r"(? list[tuple[str, str]]: def scan_line(text: str) -> list[tuple[str, str]]: + # Token bramki w linkach wewnętrznych to nie wyciek, tylko cały sens tych + # linków — a wygląda dokładnie jak sekret, na który poluje `token-hex`/ + # `token-b64`. Wycinamy WYŁĄCZNIE wartość `key=` wewnątrz atrybutu href; + # reszta linii (i każde inne `key=` w treści dokumentu) leci do skanera + # normalnie, żeby ta furtka nie zaczęła wyciszać prawdziwych sekretów. + text = _HREF_TOKEN_RE.sub(r"\1", text) + hits = _ipv4_hits(text) + _ipv6_hits(text) + _port_hits(text) hits += [("path-host", m.group(0)) for m in _PATH_RE.finditer(text)] hits += _token_hits(text) @@ -830,6 +896,15 @@ def main() -> int: default=DEFAULT_OUT, help="katalog wyjściowy (domyślnie build/kb-site)", ) + parser.add_argument( + "--access-token", + default=None, + help=( + f"token bramki NPM dopisywany jako ?{ACCESS_TOKEN_PARAM}=… do każdego " + f"linku wewnętrznego; domyślnie ze zmiennej {ACCESS_TOKEN_ENV}. " + "Bez tokenu linki zostają gołe (podgląd lokalny)." + ), + ) parser.add_argument( "--check", action="store_true", @@ -846,7 +921,10 @@ def main() -> int: if args.check: return check(args.out.resolve(), args.whitelist) - build(args.out.resolve(), args.base_url) + token = args.access_token + if token is None: + token = os.environ.get(ACCESS_TOKEN_ENV, "") + build(args.out.resolve(), args.base_url, token.strip()) return 0