Application security architecture¶
PEART is three processes with three trust boundaries, and a content model built to hold hostile input
without ever running it. The authoritative account is docs/security.md; this page summarises it for
an evaluator.
Three processes, three boundaries¶
The internal surface (peart) is where the pentester works: full read and write to the
database, access to the master key and to the evidence directory. The portal (peart-portal) is
where the client reads a delivered report: a separate process, no master key, and no access to the
evidence directory. It holds two connections to the same database file. The first, which serves
every read the client sees, carries mode=ro in its connection string, so the refusal comes from
the SQLite driver itself rather than from filesystem permissions, which do not work with WAL. The
second is genuinely read-write, and it exists because a session cannot be created, touched or
revoked through a read-only connection; what narrows it is not the connection string but a Go
interface with a fixed set of methods, so the store's other write methods are unreachable through it
rather than merely unused. Compromising the portal cannot yield the key, the original evidence,
internal-only fields, or a write to report content: the writes it can reach are its own session
rows, its own audit entries, and the password and optional second factor of the account signing in.
The rendering sidecar turns report HTML into PDF with WeasyPrint, and it processes HTML derived from hostile content, so it is treated as attack surface. It runs with no network egress, no database, no key and no evidence access; it receives a working directory of already-resolved HTML and local assets and returns a PDF. This isolation is not optional, and the systemd units carry it.
Hostile content, never executed¶
Every text field is hostile input. PEART stores, displays, exports and prints payloads across five
distinct output contexts without executing them, escaping per context rather than once and hoping. Rich
text is a restricted Markdown dialect rather than arbitrary HTML. Exported spreadsheets neutralise
formula injection: a field beginning with = is prefixed and written as a text type, so a finding
title never becomes a live formula in a client's spreadsheet.
The front end enforces this in the browser too. The Content-Security-Policy carries no unsafe-inline
and no unsafe-eval; the components are Lit in light DOM; and the project's own code contains zero
occurrences of innerHTML, unsafeHTML or unsafeStatic. A browser test loads the real page shell
and fails if the policy is actually violated in the console, so the CSP the whole front end rests on
cannot regress unnoticed.
Evidence ingestion¶
Uploaded evidence is not trusted as-is. The image's real dimensions are read from its header, and the image is decoded and re-encoded rather than stored verbatim; a format without safe pure-Go support is re-encoded to PNG rather than passed through. The pipeline gives the renderer HTML that cannot carry an external reference, closing the server-side request forgery and local-file-read vectors at the point where hostile content meets the renderer.
Authorization¶
Access control is enforced by mandatory scoping on every query, with denial by default. The boundary that matters is a client reaching another client's engagement through the portal, and it is closed by requiring the scope on the query itself rather than by a check a route could forget.