Source code for montin.core.security
"""
montin.core.security
======================
Opt-in security / privacy hardening for a generated report, grouped into one
:class:`Security` object passed as ``Deck(security=...)``.
Montin's whole point is a report that transfers **no data** and works fully
offline. These switches let you turn that into an enforced, verifiable property
instead of just a default. Every field is documented in plain language because
the underlying web terms (CSP, SRI, Permissions-Policy) are easy to misjudge —
see each attribute below and ``docs/sections/security.md``.
"""
from __future__ import annotations
from dataclasses import dataclass
# Strict Content-Security-Policy used by ``block_external``. ``default-src
# 'none'`` forbids every network destination (scripts, styles, images, fonts,
# XHR/fetch/websockets, frames...) unless re-allowed below. We re-allow only
# *inline* code and ``data:`` URIs — never an external origin — because Montin
# inlines its own JS/CSS and embeds images as data URIs. The net effect: the
# page cannot make a single outbound request, so it cannot leak data or phone
# home, even when double-clicked from disk.
STRICT_CSP = (
"default-src 'none'; "
"script-src 'unsafe-inline'; "
"style-src 'unsafe-inline'; "
"img-src data: blob:; "
"font-src data:; "
"media-src data: blob:; "
"connect-src 'none'; "
"base-uri 'none'; "
"form-action 'none'"
)
# Sensible default for ``permissions_policy=True``: declare that the report needs
# none of these device features.
DEFAULT_PERMISSIONS_POLICY = (
"camera=(), microphone=(), geolocation=(), "
"usb=(), payment=(), interest-cohort=()"
)
[docs]
@dataclass
class Security:
"""Security / privacy options for a report.
The default ``Security()`` applies light, always-safe hardening (a
no-referrer hint, a Permissions-Policy declaration, and integrity hashes on
any CDN-loaded library). The strong guarantee — a report that provably makes
no external request — is the opt-in ``block_external``.
Attributes:
block_external: **Hard offline guarantee.** Embeds every library in the
file, tells the browser (via a strict Content-Security-Policy) to
refuse *any* external request, and then re-reads the finished HTML to
prove not a single external URL remains — raising instead of writing
if one does. Use for air-gapped / regulated environments where the
report must provably phone home to nobody. Forces all plugins to
``bundled`` (and errors if a plugin was explicitly set to ``"cdn"``).
sri: **Tamper-check for CDN libraries.** When a library loads from a CDN,
the browser compares the downloaded file against a fingerprint baked
into the page; if a hacked or man-in-the-middled CDN serves altered
code, the fingerprint won't match and the browser refuses to run it.
No effect on bundled plugins.
no_referrer: **Don't leak where the report came from.** When the report
is hosted and someone clicks a link in it, the browser won't tell the
destination site which report URL they came from — avoiding leaks of
possibly-sensitive internal paths. (Opening from disk has no referrer
anyway, so this only matters when the report is served.)
noindex: **Keep it out of search engines.** If the report is hosted on
the web, asks crawlers not to list it, so an internal report doesn't
surface on Google. No effect for a file opened from disk.
permissions_policy: **Declare no camera / microphone / location use.**
``True`` emits a sensible "all sensors off" policy, a string sets a
custom one, ``False`` omits it. **Caveat:** browsers only *enforce*
this when a server sends it as an HTTP header; opened straight from
disk (``file://``) the ``<meta>`` form is ignored, so here it is a
documented statement of intent that becomes a real control only when
the report is served behind a header-setting host.
csp: **Advanced.** Supply your own Content-Security-Policy string to take
full control of what the page may load. Overrides the policy Montin
would generate.
"""
block_external: bool = False
sri: bool = True
no_referrer: bool = True
noindex: bool = False
permissions_policy: bool | str = True
csp: str | None = None
[docs]
def csp_content(self) -> str | None:
"""The Content-Security-Policy to emit, or ``None`` for no CSP meta tag.
An explicit ``csp`` wins; otherwise ``block_external`` emits the strict
no-egress policy; otherwise no CSP is set.
"""
if self.csp is not None:
return self.csp
if self.block_external:
return STRICT_CSP
return None
[docs]
def permissions_policy_content(self) -> str | None:
"""The Permissions-Policy string to emit, or ``None`` to omit it."""
if self.permissions_policy is True:
return DEFAULT_PERMISSIONS_POLICY
if isinstance(self.permissions_policy, str):
return self.permissions_policy
return None