mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-21 07:41:28 +02:00
Repo-wide sweep of every user-facing string: frontend components, routes, config and stores; API details and route docs; shared definitions and models; stage config help; report prose; MCP, toolbox, interest and connector text; notification and worker messages. Descriptions that explained, reassured, justified or restated their title are deleted. Confirm dialogs name what is removed and stop. Failed actions read "<Noun> not <verbed>", successes "<Noun> <verbed>". No reader address, no product-as-subject, no em-dashes or parentheticals in prose. Mirrors updated on both sides: EPSS band help, compare refusal reasons, interest labels. Four tests asserting on old wording follow the new text. The rules are condensed into CLAUDE.md under Writing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SK5o8xR96cbSnAfjYSTPSe
484 lines
14 KiB
Python
484 lines
14 KiB
Python
"""The report vocabulary: formats, scopes, page setup and the customisation surface."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
MAX_SECTIONS = 60
|
|
MAX_CUSTOM_BLOCKS = 20
|
|
MAX_TITLE_LENGTH = 200
|
|
MAX_TEXT_BLOCK = 20_000
|
|
MAX_LOGO_BYTES = 512_000
|
|
MAX_REPORT_ROWS = 5_000
|
|
MAX_EVIDENCE_CHARS = 4_000
|
|
MAX_SCREENSHOTS = 60
|
|
MAX_IMAGE_PIXELS = 30_000_000
|
|
REPORT_ROOT = "/app/reports-out"
|
|
RETENTION_DAYS = 90
|
|
|
|
|
|
class ReportFormat(StrEnum):
|
|
PDF = "pdf"
|
|
HTML = "html"
|
|
MARKDOWN = "markdown"
|
|
JSON = "json"
|
|
|
|
|
|
REPORT_FORMATS: tuple[str, ...] = tuple(f.value for f in ReportFormat)
|
|
|
|
FORMAT_LABELS: dict[str, str] = {
|
|
ReportFormat.PDF.value: "PDF",
|
|
ReportFormat.HTML.value: "HTML",
|
|
ReportFormat.MARKDOWN.value: "Markdown",
|
|
ReportFormat.JSON.value: "JSON",
|
|
}
|
|
|
|
FORMAT_MEDIA_TYPES: dict[str, str] = {
|
|
ReportFormat.PDF.value: "application/pdf",
|
|
ReportFormat.HTML.value: "text/html",
|
|
ReportFormat.MARKDOWN.value: "text/markdown",
|
|
ReportFormat.JSON.value: "application/json",
|
|
}
|
|
|
|
FORMAT_EXTENSIONS: dict[str, str] = {
|
|
ReportFormat.PDF.value: "pdf",
|
|
ReportFormat.HTML.value: "html",
|
|
ReportFormat.MARKDOWN.value: "md",
|
|
ReportFormat.JSON.value: "json",
|
|
}
|
|
|
|
|
|
class ReportStatus(StrEnum):
|
|
QUEUED = "queued"
|
|
RUNNING = "running"
|
|
COMPLETED = "completed"
|
|
FAILED = "failed"
|
|
|
|
|
|
REPORT_STATUS_LABELS: dict[str, str] = {
|
|
ReportStatus.QUEUED.value: "Queued",
|
|
ReportStatus.RUNNING.value: "Generating",
|
|
ReportStatus.COMPLETED.value: "Ready",
|
|
ReportStatus.FAILED.value: "Failed",
|
|
}
|
|
|
|
TERMINAL_STATUSES: tuple[str, ...] = (
|
|
ReportStatus.COMPLETED.value,
|
|
ReportStatus.FAILED.value,
|
|
)
|
|
|
|
|
|
class ReportScope(StrEnum):
|
|
SCAN = "scan"
|
|
TARGET = "target"
|
|
|
|
|
|
REPORT_SCOPES: tuple[str, ...] = tuple(s.value for s in ReportScope)
|
|
|
|
SCOPE_LABELS: dict[str, str] = {
|
|
ReportScope.SCAN.value: "One scan",
|
|
ReportScope.TARGET.value: "A target",
|
|
}
|
|
|
|
SCOPE_HELP: dict[str, str] = {
|
|
ReportScope.SCAN.value: "Everything one run observed.",
|
|
ReportScope.TARGET.value: "The target's current surface, from the latest run covering each dimension.",
|
|
}
|
|
|
|
|
|
class SectionGroup(StrEnum):
|
|
FRONT_MATTER = "front_matter"
|
|
SUMMARY = "summary"
|
|
FINDINGS = "findings"
|
|
SURFACE = "surface"
|
|
INTELLIGENCE = "intelligence"
|
|
APPENDIX = "appendix"
|
|
|
|
|
|
SECTION_GROUP_ORDER: tuple[str, ...] = tuple(g.value for g in SectionGroup)
|
|
|
|
SECTION_GROUP_LABELS: dict[str, str] = {
|
|
SectionGroup.FRONT_MATTER.value: "Front matter",
|
|
SectionGroup.SUMMARY.value: "Summary",
|
|
SectionGroup.FINDINGS.value: "Findings",
|
|
SectionGroup.SURFACE.value: "Attack surface",
|
|
SectionGroup.INTELLIGENCE.value: "Intelligence",
|
|
SectionGroup.APPENDIX.value: "Appendices",
|
|
}
|
|
|
|
|
|
class SectionRole(StrEnum):
|
|
"""Section role."""
|
|
|
|
CONTENT = "content"
|
|
FURNITURE = "furniture"
|
|
|
|
|
|
SECTION_ROLES: tuple[str, ...] = tuple(r.value for r in SectionRole)
|
|
|
|
|
|
class Audience(StrEnum):
|
|
EXECUTIVE = "executive"
|
|
TECHNICAL = "technical"
|
|
MIXED = "mixed"
|
|
|
|
|
|
AUDIENCE_LABELS: dict[str, str] = {
|
|
Audience.EXECUTIVE.value: "Executive",
|
|
Audience.TECHNICAL.value: "Technical",
|
|
Audience.MIXED.value: "Mixed",
|
|
}
|
|
|
|
AUDIENCE_HELP: dict[str, str] = {
|
|
Audience.EXECUTIVE.value: "Business impact and decisions. No tool names or payloads.",
|
|
Audience.TECHNICAL.value: "Reproduction detail, evidence and exact remediation steps.",
|
|
Audience.MIXED.value: "An executive opening followed by full technical detail.",
|
|
}
|
|
|
|
|
|
class Depth(StrEnum):
|
|
BRIEF = "brief"
|
|
STANDARD = "standard"
|
|
DETAILED = "detailed"
|
|
|
|
|
|
DEPTH_LABELS: dict[str, str] = {
|
|
Depth.BRIEF.value: "Brief",
|
|
Depth.STANDARD.value: "Standard",
|
|
Depth.DETAILED.value: "Detailed",
|
|
}
|
|
|
|
|
|
class PageSize(StrEnum):
|
|
A4 = "a4"
|
|
LETTER = "letter"
|
|
LEGAL = "legal"
|
|
|
|
|
|
PAGE_SIZE_LABELS: dict[str, str] = {
|
|
PageSize.A4.value: "A4",
|
|
PageSize.LETTER.value: "US Letter",
|
|
PageSize.LEGAL.value: "US Legal",
|
|
}
|
|
|
|
PAGE_SIZE_CSS: dict[str, str] = {
|
|
PageSize.A4.value: "A4",
|
|
PageSize.LETTER.value: "Letter",
|
|
PageSize.LEGAL.value: "Legal",
|
|
}
|
|
|
|
PAGE_WIDTH_MM: dict[str, float] = {
|
|
PageSize.A4.value: 210.0,
|
|
PageSize.LETTER.value: 215.9,
|
|
PageSize.LEGAL.value: 215.9,
|
|
}
|
|
|
|
|
|
class Orientation(StrEnum):
|
|
PORTRAIT = "portrait"
|
|
LANDSCAPE = "landscape"
|
|
|
|
|
|
ORIENTATION_LABELS: dict[str, str] = {
|
|
Orientation.PORTRAIT.value: "Portrait",
|
|
Orientation.LANDSCAPE.value: "Landscape",
|
|
}
|
|
|
|
|
|
class Density(StrEnum):
|
|
COMPACT = "compact"
|
|
NORMAL = "normal"
|
|
RELAXED = "relaxed"
|
|
|
|
|
|
DENSITY_LABELS: dict[str, str] = {
|
|
Density.COMPACT.value: "Compact",
|
|
Density.NORMAL.value: "Normal",
|
|
Density.RELAXED.value: "Relaxed",
|
|
}
|
|
|
|
DENSITY_SCALE: dict[str, float] = {
|
|
Density.COMPACT.value: 0.85,
|
|
Density.NORMAL.value: 1.0,
|
|
Density.RELAXED.value: 1.18,
|
|
}
|
|
|
|
|
|
class Classification(StrEnum):
|
|
NONE = ""
|
|
PUBLIC = "Public"
|
|
INTERNAL = "Internal"
|
|
CONFIDENTIAL = "Confidential"
|
|
RESTRICTED = "Restricted"
|
|
|
|
|
|
CLASSIFICATIONS: tuple[str, ...] = tuple(c.value for c in Classification)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FontFamily:
|
|
key: str
|
|
label: str
|
|
stack: str
|
|
role: str
|
|
note: str = ""
|
|
|
|
|
|
FONT_FAMILIES: tuple[FontFamily, ...] = (
|
|
FontFamily("inter", "Inter", "Inter", "sans", "Neutral UI sans."),
|
|
FontFamily(
|
|
"space-grotesk", "Space Grotesk", "Space Grotesk", "sans", "Display sans."
|
|
),
|
|
FontFamily("ibm-plex-sans", "IBM Plex Sans", "IBM Plex Sans", "sans", "Technical."),
|
|
FontFamily(
|
|
"source-serif", "Source Serif 4", "Source Serif 4", "serif", "Document serif."
|
|
),
|
|
FontFamily(
|
|
"jetbrains-mono",
|
|
"JetBrains Mono",
|
|
"JetBrains Mono",
|
|
"mono",
|
|
"Code and evidence.",
|
|
),
|
|
FontFamily("ibm-plex-mono", "IBM Plex Mono", "IBM Plex Mono", "mono", "Code."),
|
|
)
|
|
|
|
FONT_BY_KEY: dict[str, FontFamily] = {f.key: f for f in FONT_FAMILIES}
|
|
SANS_FONTS: tuple[str, ...] = tuple(f.key for f in FONT_FAMILIES if f.role == "sans")
|
|
SERIF_FONTS: tuple[str, ...] = tuple(f.key for f in FONT_FAMILIES if f.role == "serif")
|
|
MONO_FONTS: tuple[str, ...] = tuple(f.key for f in FONT_FAMILIES if f.role == "mono")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SlotToken:
|
|
token: str
|
|
label: str
|
|
|
|
|
|
# substituted into the running header and footer slots
|
|
SLOT_TOKENS: tuple[SlotToken, ...] = (
|
|
SlotToken("{title}", "Report title"),
|
|
SlotToken("{subtitle}", "Subtitle"),
|
|
SlotToken("{target}", "Target"),
|
|
SlotToken("{client}", "Client name"),
|
|
SlotToken("{company}", "Company"),
|
|
SlotToken("{classification}", "Classification"),
|
|
SlotToken("{date}", "Report date"),
|
|
SlotToken("{scan_date}", "Scan date"),
|
|
SlotToken("{document_id}", "Document ID"),
|
|
SlotToken("{version}", "Version"),
|
|
SlotToken("{section}", "Current section"),
|
|
SlotToken("{page}", "Page number"),
|
|
SlotToken("{pages}", "Total pages"),
|
|
)
|
|
|
|
SLOT_TOKEN_VALUES: tuple[str, ...] = tuple(t.token for t in SLOT_TOKENS)
|
|
|
|
DEFAULT_HEADER_LEFT = "{section}"
|
|
DEFAULT_HEADER_RIGHT = "{classification}"
|
|
DEFAULT_FOOTER_LEFT = "{title}"
|
|
DEFAULT_FOOTER_CENTER = ""
|
|
DEFAULT_FOOTER_RIGHT = "{page} / {pages}"
|
|
|
|
DEFAULT_THEME = "midnight"
|
|
|
|
ALLOWED_IMAGE_TYPES: frozenset[str] = frozenset(
|
|
{"image/png", "image/jpeg", "image/svg+xml", "image/webp", "image/gif"}
|
|
)
|
|
_DATA_IMAGE = re.compile(r"^data:(image/[a-z0-9.+-]+);base64,[A-Za-z0-9+/=\s]+$", re.I)
|
|
|
|
|
|
def validate_embedded_image(value: str, field: str) -> str:
|
|
"""An image must be embedded data of a known type."""
|
|
text = (value or "").strip()
|
|
if not text:
|
|
return ""
|
|
match = _DATA_IMAGE.match(text)
|
|
if match is None:
|
|
msg = f"{field} must be an uploaded image. Links are not accepted."
|
|
raise ValueError(msg)
|
|
if match.group(1).lower() not in ALLOWED_IMAGE_TYPES:
|
|
msg = f"{field} must be a PNG, JPEG, SVG, WebP or GIF."
|
|
raise ValueError(msg)
|
|
if len(text) > MAX_LOGO_BYTES:
|
|
msg = f"{field} is larger than the {MAX_LOGO_BYTES // 1024} KB limit."
|
|
raise ValueError(msg)
|
|
return text
|
|
|
|
|
|
DEFAULT_SEVERITY_COLORS: dict[str, str] = {
|
|
"critical": "#d40924",
|
|
"high": "#e96500",
|
|
"medium": "#d6ab1b",
|
|
"low": "#0098b7",
|
|
"info": "#a0b1b6",
|
|
"unknown": "#a0b1b6",
|
|
}
|
|
|
|
# the same ramp restepped for dark paper
|
|
DARK_SEVERITY_COLORS: dict[str, str] = {
|
|
"critical": "#ff645f",
|
|
"high": "#f98942",
|
|
"medium": "#eec651",
|
|
"low": "#26b7d3",
|
|
"info": "#85969a",
|
|
"unknown": "#85969a",
|
|
}
|
|
|
|
# the categorical scale the UI draws with, mirrors frontend/src/app.css --chart-1..5
|
|
DEFAULT_CHART_PALETTE: tuple[str, ...] = (
|
|
"#0091ad",
|
|
"#008a39",
|
|
"#943ca7",
|
|
"#c85d00",
|
|
"#cd3380",
|
|
)
|
|
DARK_CHART_PALETTE: tuple[str, ...] = (
|
|
"#21a3bc",
|
|
"#139948",
|
|
"#b25ec5",
|
|
"#d97230",
|
|
"#d8488c",
|
|
)
|
|
|
|
|
|
class ReportStyle(BaseModel):
|
|
"""Everything about how the document looks."""
|
|
|
|
model_config = ConfigDict(extra="ignore", validate_assignment=True)
|
|
|
|
theme: str = Field(default=DEFAULT_THEME, max_length=40)
|
|
page_size: str = Field(default=PageSize.A4.value, max_length=10)
|
|
orientation: str = Field(default=Orientation.PORTRAIT.value, max_length=10)
|
|
margin_top: float = Field(default=26.0, ge=5, le=60)
|
|
margin_right: float = Field(default=24.0, ge=5, le=60)
|
|
margin_bottom: float = Field(default=23.0, ge=5, le=60)
|
|
margin_left: float = Field(default=24.0, ge=5, le=60)
|
|
density: str = Field(default=Density.NORMAL.value, max_length=10)
|
|
base_font_size: float = Field(default=10.5, ge=6, le=16)
|
|
line_height: float = Field(default=1.6, ge=1.0, le=2.4)
|
|
heading_font: str = Field(default="", max_length=40)
|
|
body_font: str = Field(default="", max_length=40)
|
|
mono_font: str = Field(default="", max_length=40)
|
|
accent: str = Field(default="", max_length=9)
|
|
accent_soft: str = Field(default="", max_length=9)
|
|
severity_colors: dict[str, str] = Field(default_factory=dict)
|
|
chart_palette: list[str] = Field(default_factory=list, max_length=8)
|
|
mono_safe: bool = False
|
|
table_zebra: bool = True
|
|
section_numbering: bool = True
|
|
chapter_breaks: bool = True
|
|
figure_numbering: bool = True
|
|
page_numbers: bool = True
|
|
show_header: bool = True
|
|
show_footer: bool = True
|
|
header_left: str = Field(default=DEFAULT_HEADER_LEFT, max_length=120)
|
|
header_center: str = Field(default="", max_length=120)
|
|
header_right: str = Field(default=DEFAULT_HEADER_RIGHT, max_length=120)
|
|
footer_left: str = Field(default=DEFAULT_FOOTER_LEFT, max_length=120)
|
|
footer_center: str = Field(default=DEFAULT_FOOTER_CENTER, max_length=120)
|
|
footer_right: str = Field(default=DEFAULT_FOOTER_RIGHT, max_length=120)
|
|
cover_layout: str = Field(default="", max_length=40)
|
|
cover_image: str = Field(default="", max_length=MAX_LOGO_BYTES)
|
|
watermark_text: str = Field(default="", max_length=40)
|
|
watermark_opacity: float = Field(default=0.05, ge=0.01, le=0.4)
|
|
link_urls: bool = True
|
|
justify: bool = True
|
|
hyphenate: bool = True
|
|
|
|
@field_validator("cover_image")
|
|
@classmethod
|
|
def _cover_is_embedded(cls, value: str) -> str:
|
|
return validate_embedded_image(value, "The cover image")
|
|
|
|
|
|
class Revision(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
version: str = Field(default="", max_length=20)
|
|
date: str = Field(default="", max_length=40)
|
|
author: str = Field(default="", max_length=120)
|
|
note: str = Field(default="", max_length=500)
|
|
|
|
|
|
class ReportBranding(BaseModel):
|
|
"""Who the document is from, who it is for, and what may be done with it."""
|
|
|
|
model_config = ConfigDict(extra="ignore", validate_assignment=True)
|
|
|
|
company_name: str = Field(default="", max_length=160)
|
|
company_logo: str = Field(default="", max_length=MAX_LOGO_BYTES)
|
|
client_name: str = Field(default="", max_length=160)
|
|
prepared_for: str = Field(default="", max_length=200)
|
|
prepared_by: str = Field(default="", max_length=200)
|
|
author: str = Field(default="", max_length=160)
|
|
contact_email: str = Field(default="", max_length=200)
|
|
contact_url: str = Field(default="", max_length=300)
|
|
classification: str = Field(default="", max_length=40)
|
|
document_id: str = Field(default="", max_length=60)
|
|
version: str = Field(default="", max_length=20)
|
|
distribution: list[str] = Field(default_factory=list, max_length=20)
|
|
revisions: list[Revision] = Field(default_factory=list, max_length=20)
|
|
confidentiality_statement: str = Field(default="", max_length=2000)
|
|
disclaimer: str = Field(default="", max_length=2000)
|
|
|
|
@field_validator("company_logo")
|
|
@classmethod
|
|
def _logo_is_embedded(cls, value: str) -> str:
|
|
return validate_embedded_image(value, "The logo")
|
|
|
|
|
|
class NarrativeOptions(BaseModel):
|
|
"""How the prose is written, and whether a model writes any of it."""
|
|
|
|
model_config = ConfigDict(extra="ignore", validate_assignment=True)
|
|
|
|
ai_enabled: bool = False
|
|
audience: str = Field(default=Audience.MIXED.value, max_length=20)
|
|
depth: str = Field(default=Depth.STANDARD.value, max_length=20)
|
|
explain_findings: bool = False
|
|
max_explained_issues: int = Field(default=12, ge=1, le=40)
|
|
model: str = Field(default="", max_length=80)
|
|
disclose_ai: bool = True
|
|
house_style: str = Field(default="", max_length=1000)
|
|
|
|
|
|
class SectionEntry(BaseModel):
|
|
"""One row of the document outline."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
section: str = Field(max_length=60)
|
|
enabled: bool = True
|
|
title: str = Field(default="", max_length=MAX_TITLE_LENGTH)
|
|
config: dict = Field(default_factory=dict)
|
|
|
|
|
|
class ReportSpec(BaseModel):
|
|
"""The resolved document: what to say, about what, and how it looks."""
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
title: str = Field(default="", max_length=MAX_TITLE_LENGTH)
|
|
subtitle: str = Field(default="", max_length=MAX_TITLE_LENGTH)
|
|
scope: str = Field(default=ReportScope.SCAN.value, max_length=16)
|
|
sections: list[SectionEntry] = Field(default_factory=list, max_length=MAX_SECTIONS)
|
|
style: ReportStyle = Field(default_factory=ReportStyle)
|
|
branding: ReportBranding = Field(default_factory=ReportBranding)
|
|
narrative: NarrativeOptions = Field(default_factory=NarrativeOptions)
|
|
formats: list[str] = Field(default_factory=lambda: [ReportFormat.PDF.value])
|
|
|
|
|
|
def coerce_format(value: str | None) -> str:
|
|
key = (value or "").strip().lower()
|
|
return key if key in FORMAT_LABELS else ReportFormat.PDF.value
|
|
|
|
|
|
def coerce_scope(value: str | None) -> str:
|
|
key = (value or "").strip().lower()
|
|
return key if key in REPORT_SCOPES else ReportScope.SCAN.value
|