mirror of
https://github.com/usestrix/strix.git
synced 2026-08-19 18:13:34 +02:00
fix(reports): safe-fence markdown PoC export and share fence helpers
This commit is contained in:
@@ -39,13 +39,14 @@ from strix.interface.tui.live_view import TuiLiveView
|
||||
from strix.interface.tui.messages import send_user_message_to_agent
|
||||
from strix.interface.tui.renderers import render_tool_widget
|
||||
from strix.interface.tui.renderers.agent_message_renderer import AgentMessageRenderer
|
||||
from strix.interface.tui.renderers.fenced import (
|
||||
from strix.interface.tui.renderers.user_message_renderer import UserMessageRenderer
|
||||
from strix.interface.utils import build_tui_stats_text
|
||||
from strix.report.fenced import (
|
||||
guess_language_name,
|
||||
parse_fenced_code,
|
||||
resolve_lexer,
|
||||
safe_fence,
|
||||
)
|
||||
from strix.interface.tui.renderers.user_message_renderer import UserMessageRenderer
|
||||
from strix.interface.utils import build_tui_stats_text
|
||||
from strix.report.state import ReportState, set_global_report_state
|
||||
from strix.runtime import session_manager
|
||||
|
||||
@@ -608,9 +609,10 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
||||
if vuln.get("poc_script_code"):
|
||||
poc_language, poc_code = parse_fenced_code(vuln["poc_script_code"])
|
||||
fence_lang = poc_language or guess_language_name(poc_code)
|
||||
lines.append(f"```{fence_lang}")
|
||||
fence = safe_fence(poc_code)
|
||||
lines.append(f"{fence}{fence_lang}")
|
||||
lines.append(poc_code)
|
||||
lines.append("```")
|
||||
lines.append(fence)
|
||||
|
||||
if vuln.get("code_locations"):
|
||||
lines.extend(["", "## Code Analysis", ""])
|
||||
|
||||
@@ -5,8 +5,9 @@ from pygments.styles import get_style_by_name
|
||||
from rich.text import Text
|
||||
from textual.widgets import Static
|
||||
|
||||
from strix.report.fenced import parse_fenced_code, resolve_lexer
|
||||
|
||||
from .base_renderer import BaseToolRenderer
|
||||
from .fenced import parse_fenced_code, resolve_lexer
|
||||
from .registry import register_tool_renderer
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,21 @@ from pygments.lexers.special import TextLexer
|
||||
from pygments.util import ClassNotFound
|
||||
|
||||
|
||||
_FENCE_RE = re.compile(r"^```([^\n`]*)\n(.*?)\n?```$", re.DOTALL)
|
||||
_FENCE_RE = re.compile(r"^```([^\n`]*)\r?\n(.*?)\r?\n?```$", re.DOTALL)
|
||||
_BACKTICK_RUN = re.compile(r"`+")
|
||||
|
||||
|
||||
def safe_fence(content: str) -> str:
|
||||
"""Return a backtick fence that ``content`` cannot break out of.
|
||||
|
||||
Per CommonMark a fenced code block is closed only by a run of backticks at
|
||||
least as long as the opening fence. LLM-authored, attacker-influenced values
|
||||
(PoC scripts, code snippets) may contain their own ``` runs, so we open with
|
||||
a fence one backtick longer than the longest run inside ``content`` (never
|
||||
fewer than three). Everything in ``content`` then renders verbatim.
|
||||
"""
|
||||
longest = max((len(m.group()) for m in _BACKTICK_RUN.finditer(content)), default=0)
|
||||
return "`" * max(3, longest + 1)
|
||||
|
||||
|
||||
def parse_fenced_code(raw: str) -> tuple[str | None, str]:
|
||||
+6
-20
@@ -6,34 +6,19 @@ import csv
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import tempfile
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from strix.core.paths import run_record_path
|
||||
from strix.report.fenced import guess_language_name, parse_fenced_code, safe_fence
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
||||
|
||||
_BACKTICK_RUN = re.compile(r"`+")
|
||||
|
||||
|
||||
def _safe_fence(content: str) -> str:
|
||||
"""Return a backtick fence that ``content`` cannot break out of.
|
||||
|
||||
Per CommonMark a fenced code block is closed only by a run of backticks at
|
||||
least as long as the opening fence. LLM-authored, attacker-influenced values
|
||||
(PoC scripts, code snippets) may contain their own ``` runs, so we open with
|
||||
a fence one backtick longer than the longest run inside ``content`` (never
|
||||
fewer than three). Everything in ``content`` then renders verbatim.
|
||||
"""
|
||||
longest = max((len(m.group()) for m in _BACKTICK_RUN.finditer(content)), default=0)
|
||||
return "`" * max(3, longest + 1)
|
||||
|
||||
|
||||
def read_run_record(run_dir: Path) -> dict[str, Any]:
|
||||
path = run_record_path(run_dir)
|
||||
@@ -187,9 +172,10 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
lines.append(str(report["poc_description"]))
|
||||
lines.append("")
|
||||
if report.get("poc_script_code"):
|
||||
code = str(report["poc_script_code"])
|
||||
fence = _safe_fence(code)
|
||||
lines.append(fence)
|
||||
language, code = parse_fenced_code(str(report["poc_script_code"]))
|
||||
fence_lang = language or guess_language_name(code)
|
||||
fence = safe_fence(code)
|
||||
lines.append(f"{fence}{fence_lang}")
|
||||
lines.append(code)
|
||||
lines.append(fence)
|
||||
lines.append("")
|
||||
@@ -209,7 +195,7 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
lines.append(f" {loc['label']}")
|
||||
if loc.get("snippet"):
|
||||
snippet = str(loc["snippet"])
|
||||
fence = _safe_fence(snippet)
|
||||
fence = safe_fence(snippet)
|
||||
lines.append(f" {fence}")
|
||||
lines.extend(f" {ln}" for ln in snippet.splitlines())
|
||||
lines.append(f" {fence}")
|
||||
|
||||
@@ -17,7 +17,7 @@ export function MdCodeBlock({
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const raw = String(children).replace(/\n$/, "");
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const match = /language-(\S+)/.exec(className || "");
|
||||
const isBlock = raw.includes("\n") || match;
|
||||
|
||||
if (!isBlock) {
|
||||
|
||||
@@ -3,7 +3,7 @@ export interface ParsedFencedCode {
|
||||
code: string;
|
||||
}
|
||||
|
||||
const FENCE_RE = /^```([^\n`]*)\n([\s\S]*?)\n?```$/;
|
||||
const FENCE_RE = /^```([^\n`]*)\r?\n([\s\S]*?)\r?\n?```$/;
|
||||
|
||||
/**
|
||||
* Agent-generated `poc_script_code` is stored wrapped in a markdown code fence
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>Strix Results</title>
|
||||
<script type="module" crossorigin src="./assets/index-B1jJGKIO.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-Dd1cyttN.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-vV8wxCG6.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -4,10 +4,11 @@ from __future__ import annotations
|
||||
|
||||
from pygments.lexers import BashLexer, PythonLexer
|
||||
|
||||
from strix.interface.tui.renderers.fenced import (
|
||||
from strix.report.fenced import (
|
||||
guess_language_name,
|
||||
parse_fenced_code,
|
||||
resolve_lexer,
|
||||
safe_fence,
|
||||
)
|
||||
from strix.viewer.report_pdf import _strip_code_fence
|
||||
|
||||
@@ -62,3 +63,16 @@ def test_resolve_lexer_falls_back_to_python_when_unresolvable() -> None:
|
||||
|
||||
def test_guess_language_name_defaults_to_python_when_inconclusive() -> None:
|
||||
assert guess_language_name("") == "python"
|
||||
|
||||
|
||||
def test_parse_fenced_code_handles_crlf() -> None:
|
||||
language, code = parse_fenced_code("```python\r\nx = 1\r\n```")
|
||||
assert language == "python"
|
||||
assert code == "x = 1"
|
||||
|
||||
|
||||
def test_safe_fence_widens_past_embedded_backticks() -> None:
|
||||
# A PoC body containing a ``` run must be wrapped in a longer fence so it
|
||||
# can't terminate the block early.
|
||||
assert safe_fence("plain code") == "```"
|
||||
assert safe_fence("has ```\nfence inside") == "````"
|
||||
|
||||
@@ -119,9 +119,11 @@ def test_render_vulnerability_md_poc_code_cannot_break_out_of_fence() -> None:
|
||||
injected = "curl x\n```\n\n## Injected Heading\n"
|
||||
md = render_vulnerability_md(_sample_report(poc_script_code=injected))
|
||||
lines = md.split("\n")
|
||||
fence = next(ln for ln in lines[lines.index("## Proof of Concept") + 1 :] if ln.strip())
|
||||
assert set(fence) == {"`"}
|
||||
assert len(fence) >= 4 # wider than the payload's 3-backtick run
|
||||
opening = next(ln for ln in lines[lines.index("## Proof of Concept") + 1 :] if ln.strip())
|
||||
ticks = opening[: len(opening) - len(opening.lstrip("`"))]
|
||||
assert len(ticks) >= 4 # wider than the payload's 3-backtick run
|
||||
assert "`" not in opening.removeprefix(ticks) # backtick run + language tag only
|
||||
assert f"\n{ticks}\n" in md # pure-backtick closing fence of the same width
|
||||
assert injected in md # the payload survives verbatim, inside the fence
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user