90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""Renders the SAST/SCA report template.
|
|
|
|
The template and its stylesheet are the design; this module is only what turns them into a
|
|
document. It takes a context that is already shaped for the page -- every number formatted, every
|
|
bar already a percentage -- so that the template places strings and never computes.
|
|
|
|
Nothing here reads an artifact. Filling a context from a scan is :mod:`context`, which works from
|
|
what :mod:`sources` collected; :func:`sample.sample_context` builds one by hand, which is what the
|
|
template tests render.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
|
|
TEMPLATE_DIR = Path(__file__).resolve().parent / "template"
|
|
|
|
TEMPLATE_NAME = "report.html.j2"
|
|
|
|
STYLESHEET = TEMPLATE_DIR / "report.css"
|
|
|
|
# What the document calls itself, in the PDF's own metadata rather than only on its first page.
|
|
TITLE = "Complete Security Findings Report"
|
|
|
|
AUTHOR = "CipherCell"
|
|
|
|
# The shape :func:`render_html` expects. Written down because the template guards on it: a key that
|
|
# is absent leaves its section out, which is how a scan that ran half the pipeline produces a
|
|
# shorter report rather than one full of empty tables.
|
|
CONTEXT = {
|
|
"cover": "scan_id, footer_date, logo, title, identity[], severity_ledger{title, rows[]}",
|
|
"project": "title, overview[], languages{title, rows[]}",
|
|
"summary": "title, overview[], distributions[{title, rows[], legend}]",
|
|
"critical": "title, table{columns[], rows[]}",
|
|
"sections": "[{title, new_page?, groups[{title, note?, cards[], table?}]}]",
|
|
}
|
|
|
|
|
|
def render_html(context: dict[str, Any]) -> str:
|
|
"""Render the report as an HTML document.
|
|
|
|
Args:
|
|
context: The page-ready context; see :data:`CONTEXT`.
|
|
|
|
Returns:
|
|
The rendered HTML.
|
|
"""
|
|
environment = Environment(
|
|
loader=FileSystemLoader(TEMPLATE_DIR),
|
|
autoescape=select_autoescape(default_for_string=True, default=True),
|
|
trim_blocks=True,
|
|
lstrip_blocks=True,
|
|
)
|
|
|
|
return environment.get_template(TEMPLATE_NAME).render(**context)
|
|
|
|
|
|
def render_pdf(context: dict[str, Any]) -> bytes:
|
|
"""Render the report as a PDF.
|
|
|
|
Args:
|
|
context: The page-ready context; see :data:`CONTEXT`.
|
|
|
|
Returns:
|
|
The PDF.
|
|
"""
|
|
# WeasyPrint pulls in native cairo and pango bindings. Imported here so that importing this
|
|
# module -- to render the HTML, or merely to read CONTEXT -- does not pay for them.
|
|
from weasyprint import CSS, HTML
|
|
from weasyprint.text.fonts import FontConfiguration
|
|
|
|
# Without this the @font-face rules are ignored and Space Grotesk is silently replaced by
|
|
# whatever the host happens to have, which is how the design sample came to be set in Carlito.
|
|
fonts = FontConfiguration()
|
|
|
|
# Relative URLs in the template and the stylesheet -- the mark, the font files -- resolve
|
|
# against the template directory.
|
|
document = HTML(string=render_html(context), base_url=str(TEMPLATE_DIR)).render(
|
|
stylesheets=[CSS(filename=str(STYLESHEET), font_config=fonts)],
|
|
font_config=fonts,
|
|
)
|
|
|
|
document.metadata.title = f"{TITLE} - {context.get('cover', {}).get('subject', '')}".strip(" -")
|
|
document.metadata.authors = [AUTHOR]
|
|
|
|
return bytes(document.write_pdf())
|