mirror of
https://github.com/usestrix/strix.git
synced 2026-08-28 05:25:00 +00:00
Merge bc1b0b7ec2 into 717ffc8f4c
This commit is contained in:
commit
7ab270fc35
2 changed files with 78 additions and 6 deletions
|
|
@ -417,10 +417,15 @@ def _inline_md(text: str) -> str:
|
|||
codes.append(match.group(1))
|
||||
return f"\x00{len(codes) - 1}\x00"
|
||||
|
||||
# Bold spans must start and end with a non-delimiter, so a run of asterisks such as a masked
|
||||
# secret (``******``) stays literal. Italic content is plain text or complete ``<b>..</b>``
|
||||
# spans, so the passes can nest (``**a *b* c**``, ``*a **b** c*``) but can never interleave
|
||||
# into crossed markup (``<b><i>..</b></i>``), which reportlab rejects.
|
||||
seg = html.escape(re.sub(r"`([^`]+)`", _stash, text))
|
||||
seg = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", seg)
|
||||
seg = re.sub(r"__(.+?)__", r"<b>\1</b>", seg)
|
||||
seg = re.sub(r"\*(.+?)\*", r"<i>\1</i>", seg)
|
||||
seg = re.sub(r"\*\*\*(?=[^*])(.+?)(?<=[^*])\*\*\*", r"<b><i>\1</i></b>", seg)
|
||||
seg = re.sub(r"\*\*(?=[^*])(.+?)(?<=[^*])\*\*", r"<b>\1</b>", seg)
|
||||
seg = re.sub(r"__(?=[^_])(.+?)(?<=[^_])__", r"<b>\1</b>", seg)
|
||||
seg = re.sub(r"\*((?:[^*<>\n]|<b>[^<>*\n]*</b>)+?)\*", r"<i>\1</i>", seg)
|
||||
|
||||
def _restore(match: re.Match[str]) -> str:
|
||||
inner = html.escape(codes[int(match.group(1))])
|
||||
|
|
@ -429,6 +434,14 @@ def _inline_md(text: str) -> str:
|
|||
return re.sub(r"\x00(\d+)\x00", _restore, seg)
|
||||
|
||||
|
||||
def _para(markup: str, style: ParagraphStyle) -> Paragraph:
|
||||
"""Build a Paragraph; if reportlab rejects the markup, drop our tags and keep the text."""
|
||||
try:
|
||||
return Paragraph(markup, style)
|
||||
except ValueError:
|
||||
return Paragraph(re.sub(r"</?(?:b|i|font)\b[^>]*>", "", markup), style)
|
||||
|
||||
|
||||
def _strip_leading_heading(md: str) -> str:
|
||||
"""Drop a single leading markdown heading (each section adds its own title)."""
|
||||
lines = md.lstrip("\n").split("\n")
|
||||
|
|
@ -447,12 +460,12 @@ def _markdown_flowables( # noqa: PLR0915 - cohesive block parser, splitting hur
|
|||
|
||||
def flush_para() -> None:
|
||||
if para:
|
||||
flow.append(Paragraph(_inline_md(" ".join(para)), styles["body"]))
|
||||
flow.append(_para(_inline_md(" ".join(para)), styles["body"]))
|
||||
para.clear()
|
||||
|
||||
def flush_bullets() -> None:
|
||||
for marker, item in bullets:
|
||||
flow.append(Paragraph(f"{marker} {_inline_md(item)}", styles["bullet"]))
|
||||
flow.append(_para(f"{marker} {_inline_md(item)}", styles["bullet"]))
|
||||
bullets.clear()
|
||||
|
||||
lines = md.replace("\r\n", "\n").split("\n")
|
||||
|
|
@ -479,7 +492,7 @@ def _markdown_flowables( # noqa: PLR0915 - cohesive block parser, splitting hur
|
|||
if heading:
|
||||
flush_para()
|
||||
flush_bullets()
|
||||
flow.append(Paragraph(_inline_md(heading.group(2)), styles["md_heading"]))
|
||||
flow.append(_para(_inline_md(heading.group(2)), styles["md_heading"]))
|
||||
i += 1
|
||||
continue
|
||||
ordered = re.match(r"^(\d+)\.\s+(.*)$", stripped)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,14 @@ from typing import TYPE_CHECKING
|
|||
import pytest
|
||||
from pypdf import PdfReader
|
||||
from pypdf.errors import WrongPasswordError
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
from strix.interface.viewer.report_pdf import (
|
||||
_inline_md,
|
||||
_markdown_flowables,
|
||||
_para,
|
||||
_styles,
|
||||
build_encrypted_report,
|
||||
encrypt_pdf,
|
||||
generate_password,
|
||||
|
|
@ -103,3 +109,56 @@ def test_build_encrypted_report(tmp_path: Path) -> None:
|
|||
reader = PdfReader(BytesIO(pdf_bytes))
|
||||
assert reader.is_encrypted
|
||||
assert reader.decrypt(password)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
[
|
||||
"(observed as '******')", # masked secret: a run of asterisks
|
||||
"***x***",
|
||||
"**a *b** c*",
|
||||
"* * *",
|
||||
"__a *b__ c*",
|
||||
],
|
||||
)
|
||||
def test_inline_md_never_emits_crossed_markup(text: str) -> None:
|
||||
Paragraph(_inline_md(text), ParagraphStyle("t")) # reportlab raises ValueError on crossed tags
|
||||
|
||||
|
||||
def test_markdown_flowables_survive_unbalanced_emphasis() -> None:
|
||||
md = (
|
||||
"Spring Boot masks secrets in /actuator/env (observed as '******').\n\n"
|
||||
"- bullet with ***three*** stars\n"
|
||||
"# heading with *unbalanced\n"
|
||||
)
|
||||
assert len(_markdown_flowables(md, _styles())) == 3
|
||||
|
||||
|
||||
def test_generate_report_pdf_with_masked_secret_in_summary(tmp_path: Path) -> None:
|
||||
run_dir = tmp_path / "strix_runs" / "masked"
|
||||
run_dir.mkdir(parents=True)
|
||||
record = {
|
||||
"run_name": "masked",
|
||||
"targets_info": [{"original": "https://example.com"}],
|
||||
"scan_mode": "deep",
|
||||
"status": "completed",
|
||||
"start_time": "2026-01-01T00:00:00Z",
|
||||
"end_time": "2026-01-01T01:02:03Z",
|
||||
"scan_results": {
|
||||
"executive_summary": "Password keys are masked (observed as '******').",
|
||||
"recommendations": "Nothing.",
|
||||
},
|
||||
}
|
||||
(run_dir / "run.json").write_text(json.dumps(record), encoding="utf-8")
|
||||
assert generate_report_pdf(run_dir).startswith(b"%PDF")
|
||||
|
||||
|
||||
def test_para_falls_back_to_plain_text_on_crossed_markup() -> None:
|
||||
para = _para("<b><i>x</b></i> & y", ParagraphStyle("t"))
|
||||
assert para.getPlainText() == "x & y"
|
||||
|
||||
|
||||
def test_inline_md_keeps_balanced_nested_emphasis() -> None:
|
||||
assert _inline_md("**bold with *italic* inside**") == "<b>bold with <i>italic</i> inside</b>"
|
||||
assert _inline_md("*outer **bold** inner*") == "<i>outer <b>bold</b> inner</i>"
|
||||
assert "******" in _inline_md("(observed as '******')") # a masked secret stays literal
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue