Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 68 additions & 15 deletions agentprof/report/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
DEFAULT_REPORT_DIR = APP_DIR / "reports"
REPORT_ID_MAX_LENGTH = 128
REPORT_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]*")
MARKDOWN_ESCAPE_RE = re.compile(r"([\\`*\[\]()!|])")
MARKDOWN_AUTO_LINK_RE = re.compile(r"(?i)\b((?:https?|ftp)://)")
MARKDOWN_WWW_LINK_RE = re.compile(r"(?i)\bwww\.")
MULTI_AGENT_WASTE_KIND = "multi_agent_waste"


Expand Down Expand Up @@ -217,10 +220,10 @@ def _cost_to_json(record: CostLedgerRecord) -> dict[str, Any]:
def _markdown_report(payload: dict[str, Any]) -> str:
summary = payload["summary"]
lines = [
f"# AgentProf Report: {payload['project']}",
f"# AgentProf Report: {_markdown_text(payload['project'])}",
"",
f"Report ID: `{payload['report_id']}`",
f"Generated at: `{payload['generated_at']}`",
f"Report ID: {_markdown_inline_code(payload['report_id'])}",
f"Generated at: {_markdown_inline_code(payload['generated_at'])}",
"",
"## Summary",
"",
Expand All @@ -241,7 +244,7 @@ def _markdown_report(payload: dict[str, Any]) -> str:
[
"## Visuals",
"",
f"![Multi-agent waste estimate]({multi_agent_svg})",
f"![Multi-agent waste estimate]({_markdown_link_target(multi_agent_svg)})",
"",
]
)
Expand All @@ -266,10 +269,10 @@ def _markdown_report(payload: dict[str, Any]) -> str:
for record in payload["cost_ledger"]:
lines.append(
"| "
f"{record['cost_type']} | "
f"{_markdown_table_cell(record['cost_type'])} | "
f"{_format_usd(record['amount_usd'])} | "
f"{record['attribution_method']} | "
f"{record['issue_id'] or ''} |"
f"{_markdown_table_cell(record['attribution_method'])} | "
f"{_markdown_table_cell(record['issue_id'] or '')} |"
)
lines.append("")

Expand All @@ -278,31 +281,34 @@ def _markdown_report(payload: dict[str, Any]) -> str:

def _markdown_issue(issue: dict[str, Any]) -> list[str]:
lines = [
f"### {issue['title']}",
f"### {_markdown_text(issue['title'])}",
"",
f"- Issue ID: `{issue['issue_id']}`",
f"- Kind: `{issue['kind']}`",
f"- Severity: `{issue['severity']}`",
f"- Confidence: `{issue['confidence']}`",
f"- Issue ID: {_markdown_inline_code(issue['issue_id'])}",
f"- Kind: {_markdown_inline_code(issue['kind'])}",
f"- Severity: {_markdown_inline_code(issue['severity'])}",
f"- Confidence: {_markdown_inline_code(issue['confidence'])}",
f"- Affected traces: {issue['affected_traces']}",
f"- Affected spans: {issue['affected_spans']}",
f"- Wasted cost: {_format_usd(issue['wasted_cost_usd'])}",
f"- Potential savings: {_format_usd(issue['potential_savings_usd'])}",
"",
f"Recommendation: {issue['recommendation']}",
f"Recommendation: {_markdown_text(issue['recommendation'])}",
"",
]
if issue["recommended_tests"]:
lines.extend(["Recommended tests:", ""])
lines.extend(f"- {test}" for test in issue["recommended_tests"])
lines.extend(f"- {_markdown_text(test)}" for test in issue["recommended_tests"])
lines.append("")
if issue["evidence"]:
lines.extend(["Evidence:", ""])
for item in issue["evidence"]:
location = ":".join(
part for part in (item["trace_id"], item["span_id"]) if part
)
lines.append(f"- `{location}` {item['message']}")
lines.append(
f"- {_markdown_inline_code(location or 'unknown')} "
f"{_markdown_text(item['message'])}"
)
lines.append("")
return lines

Expand Down Expand Up @@ -444,6 +450,53 @@ def _agent_label(visual: dict[str, Any]) -> str:
return str(count)


def _markdown_text(value: Any) -> str:
return _escape_markdown(_single_line(value))


def _markdown_table_cell(value: Any) -> str:
return _markdown_text(value)


def _markdown_inline_code(value: Any) -> str:
text = _single_line(value)
max_backticks = max(
(len(match.group(0)) for match in re.finditer(r"`+", text)),
default=0,
)
marker = "`" * (max_backticks + 1)
padding = " " if text.startswith("`") or text.endswith("`") else ""
return f"{marker}{padding}{text}{padding}{marker}"


def _markdown_link_target(value: Any) -> str:
text = _single_line(value)
return (
text.replace("\\", "%5C")
.replace(" ", "%20")
.replace("(", "%28")
.replace(")", "%29")
)


def _escape_markdown(value: str) -> str:
text = escape(value, quote=False)
text = MARKDOWN_ESCAPE_RE.sub(r"\\\1", text)
text = MARKDOWN_AUTO_LINK_RE.sub(
lambda match: match.group(1).replace(":", ":"),
text,
)
text = MARKDOWN_WWW_LINK_RE.sub(
lambda match: match.group(0).replace(".", "."),
text,
)
return text


def _single_line(value: Any) -> str:
return "" if value is None else " ".join(str(value).split())


def _default_report_id(generated_at: datetime) -> str:
return f"agentprof-{generated_at.astimezone(UTC).strftime('%Y%m%dT%H%M%S%fZ')}"

Expand Down
74 changes: 74 additions & 0 deletions tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,37 @@ def test_generate_report_handles_empty_analysis_results(tmp_path: Path) -> None:
assert store.stats()["reports"] == 1


def test_generate_report_escapes_markdown_injection(tmp_path: Path) -> None:
store = DuckDBStore(tmp_path / "agentprof.duckdb")
_seed_markdown_injection_issue(store)

result = generate_report(
store,
project="Tracer <b>[link](http://project.example)</b>",
output_dir=tmp_path / "reports",
report_id="escaped-report",
generated_at=_dt("2026-04-26T12:00:00+00:00"),
)
markdown = result.report_md_path.read_text(encoding="utf-8")

assert "<b>" not in markdown
assert "<script" not in markdown
assert "<img" not in markdown
assert "![](http" not in markdown
assert "](http" not in markdown
assert "[ref]: http" not in markdown
assert "[evil]: http" not in markdown
assert "http://" not in markdown
assert "https://" not in markdown
assert "www." not in markdown
assert "wasted|retry" not in markdown
assert "``trace`id:span`id``" in markdown
assert "https&#58;//test.example" in markdown
assert "www&#46;test.example" in markdown
assert r"\[ref\]: http&#58;//evil.example" in markdown
assert r"wasted\|retry" in markdown


def test_generate_report_rejects_unsafe_report_id(tmp_path: Path) -> None:
store = DuckDBStore(tmp_path / "agentprof.duckdb")

Expand Down Expand Up @@ -606,5 +637,48 @@ def _seed_multi_agent_issue(store: DuckDBStore) -> None:
)


def _seed_markdown_injection_issue(store: DuckDBStore) -> None:
issue = IssueRecord(
issue_id="retry_loop:evil|id",
kind="retry_loop",
title="Pwn](http://evil.example) <script>alert(1)</script>",
severity="medium",
confidence="high",
first_seen=_dt("2026-04-26T10:00:00+00:00"),
last_seen=_dt("2026-04-26T10:00:02+00:00"),
affected_traces=1,
affected_spans=1,
total_cost_usd=Decimal("0.030"),
wasted_cost_usd=Decimal("0.020"),
potential_savings_usd=Decimal("0.020"),
recommendation="![](http://attacker.example/leak)\n[evil]: http://evil.example",
recommended_tests=["[click me](https://test.example) www.test.example `danger`"],
)
evidence = IssueEvidenceRecord(
issue_id=issue.issue_id,
trace_id="trace`id",
span_id="span`id",
evidence_type="retry_attempt",
message="`code` ![](http://attacker.example/img.png)\n[ref]: http://evil.example <img src=x>",
attributes={},
)
cost = CostLedgerRecord(
trace_id="trace`id",
span_id="span`id",
issue_id=issue.issue_id,
cost_type="wasted|retry\n[ref]: http://evil.example",
amount_usd=Decimal("0.020"),
attribution_method="retry_loop](http://evil.example)",
confidence="source",
)
store.replace_analysis_results(
issue_kind="retry_loop",
attribution_method="retry_loop](http://evil.example)",
issues=[issue],
evidence=[evidence],
cost_records=[cost],
)


def _dt(value: str) -> datetime:
return datetime.fromisoformat(value).astimezone(UTC)
Loading