From bc01847536e628442c738ac967b3fb4d1c450e3a Mon Sep 17 00:00:00 2001 From: WenJing95 <126436871+WenJing95@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:16:11 +0900 Subject: [PATCH 1/6] fix(reports): keep HTML entities literal in report text --- plugins/codex-security/scripts/report_projection.py | 4 ++-- .../codex-security/tests/test_report_projection.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/plugins/codex-security/scripts/report_projection.py b/plugins/codex-security/scripts/report_projection.py index 552f6e106d..ff94cae09f 100644 --- a/plugins/codex-security/scripts/report_projection.py +++ b/plugins/codex-security/scripts/report_projection.py @@ -32,7 +32,7 @@ def _text(value: Any, fallback: str) -> str: if re.match(r"^(?:#{1,6}\s|[-*+]\s|>\s|```|~~~|\d+[.)]\s|\|)", normalized): normalized = f"Text: {normalized}" return "".join( - part if index % 2 else re.sub(r"([\\`*_\[\]<>])", r"\\\1", part) + part if index % 2 else re.sub(r"([\\`*_\[\]<>&])", r"\\\1", part) for index, part in enumerate(re.split(r"((? str: if label != value: marker = _code_fence(label, minimum=1) return f"{marker}{label}{marker}" - return re.sub(r"([\\`*_\[\]<>])", r"\\\1", value) + return re.sub(r"([\\`*_\[\]<>&])", r"\\\1", value) def _cell(value: Any) -> str: diff --git a/plugins/codex-security/tests/test_report_projection.py b/plugins/codex-security/tests/test_report_projection.py index 4a04015af0..4c49911865 100644 --- a/plugins/codex-security/tests/test_report_projection.py +++ b/plugins/codex-security/tests/test_report_projection.py @@ -803,6 +803,17 @@ def test_projection_escapes_raw_html_in_markdown() -> None: assert r'\' in markdown_text +def test_projection_keeps_html_entities_literal_in_markdown() -> None: + manifest, findings, coverage = canonical_documents() + findings["findings"][0]["summary"] = "Sanitizer decodes <script> and &lt;" + coverage["includePaths"] = ["docs/<b>.md"] + + markdown_text = PROJECTION.generate_report_markdown(manifest, findings, coverage).decode() + + assert r"Sanitizer decodes \<script\> and \&lt;" in markdown_text + assert r"- Included paths: docs/\<b\>.md" in markdown_text + + def test_projection_omits_informational_findings() -> None: manifest, findings, coverage = canonical_documents() findings["findings"][0]["severity"]["level"] = "informational" From ea8d066f0b19a762424de8d6218f727bfdc522a1 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 23 Oct 2026 18:18:25 +0000 Subject: [PATCH 2/6] fix(reports): preserve ampersands in Markdown autolinks --- .../scripts/report_projection.py | 26 ++++++++--- .../tests/test_report_projection.py | 45 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/plugins/codex-security/scripts/report_projection.py b/plugins/codex-security/scripts/report_projection.py index ff94cae09f..69b362b906 100644 --- a/plugins/codex-security/scripts/report_projection.py +++ b/plugins/codex-security/scripts/report_projection.py @@ -20,21 +20,37 @@ "needs_follow_up": "Needs follow-up", } WRITEUP_REPORT_PATH_RE = re.compile(r"^findings/([a-z0-9][a-z0-9._-]*)/\1\.md$") +INLINE_CODE_RE = r"(?]+" class ReportProjectionError(ValueError): """Raised when a canonical scan cannot be projected into a valid report.""" +def _escape_markdown(value: str, *, inline_code: bool = False) -> str: + # Locate whole URLs before matching code spans: a URL can contain backticks. + urls = [match.span() for match in re.finditer(AUTOLINK_RE, value)] + punctuation = r"[\\`*_\[\]<>&]" + pattern = f"(?P{INLINE_CODE_RE})|{punctuation}" if inline_code else punctuation + + def escape(match: re.Match[str]) -> str: + if match.lastgroup == "code": + return match[0] + # GFM autolinks keep backslashes literally, including before query separators. + if match[0] == "&" and any(start <= match.start() < end for start, end in urls): + return "&" + return "\\" + match[0] + + return re.sub(pattern, escape, value) + + def _text(value: Any, fallback: str) -> str: candidate = value if isinstance(value, str) and value.strip() else fallback normalized = " ".join(candidate.split()) if re.match(r"^(?:#{1,6}\s|[-*+]\s|>\s|```|~~~|\d+[.)]\s|\|)", normalized): normalized = f"Text: {normalized}" - return "".join( - part if index % 2 else re.sub(r"([\\`*_\[\]<>&])", r"\\\1", part) - for index, part in enumerate(re.split(r"((? list[str]: @@ -57,7 +73,7 @@ def _path_text(value: str) -> str: if label != value: marker = _code_fence(label, minimum=1) return f"{marker}{label}{marker}" - return re.sub(r"([\\`*_\[\]<>&])", r"\\\1", value) + return _escape_markdown(value) def _cell(value: Any) -> str: diff --git a/plugins/codex-security/tests/test_report_projection.py b/plugins/codex-security/tests/test_report_projection.py index 4c49911865..10c32c01c9 100644 --- a/plugins/codex-security/tests/test_report_projection.py +++ b/plugins/codex-security/tests/test_report_projection.py @@ -814,6 +814,51 @@ def test_projection_keeps_html_entities_literal_in_markdown() -> None: assert r"- Included paths: docs/\<b\>.md" in markdown_text +@pytest.mark.parametrize( + "url", + [ + "https://example.com/?first=1&second=2", + "http://example.com/?payload=<tag>&mode=1", + "www.example.com/?payload=<tag>&mode=1", + "ftp://example.com/?first=1&second=2", + "https://example.com/`a`?first=1&second=2", + "www.example.com/`a`?first=1&second=2", + ], +) +def test_projection_preserves_bare_url_ampersands_in_prose_and_tables(url: str) -> None: + manifest, findings, coverage = canonical_documents() + finding = findings["findings"][0] + finding["summary"] = f"R&D decoded <tag>. Reproduce with {url} then compare &." + finding["confidence"]["rationale"] = f"Request: {url}" + + markdown = PROJECTION.generate_report_markdown(manifest, findings, coverage).decode() + + assert f"Reproduce with {url} then compare \\&." in markdown + assert f"| Confidence rationale | Request: {url} |" in markdown + assert r"R\&D decoded \<tag\>." in markdown + + +def test_projection_preserves_inline_code_and_existing_url_escapes() -> None: + manifest, findings, coverage = canonical_documents() + findings["findings"][0]["summary"] = ( + "Compare `&` and `https://example.com/?a=1&b=2 &` with " + "https://example.com/a_b?first=1&second=2 and &. " + "Keep https://example.com/`a _b_` as written." + ) + coverage["includePaths"] = [ + "docs/www.example.com/<tag>.md", + "docs/`<tag>`.md", + ] + + markdown = PROJECTION.generate_report_markdown(manifest, findings, coverage).decode() + + assert "Compare `&` and `https://example.com/?a=1&b=2 &`" in markdown + assert r"https://example.com/a\_b?first=1&second=2 and \&." in markdown + assert "Keep https://example.com/`a _b_` as written." in markdown + assert r"- Included paths: docs/www.example.com/\<tag\>.md" in markdown + assert r"docs/\`\<tag\>\`.md" in markdown + + def test_projection_omits_informational_findings() -> None: manifest, findings, coverage = canonical_documents() findings["findings"][0]["severity"]["level"] = "informational" From 3f6aa7919906dd7344607f6c31e92d091b1a678c Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 23 Oct 2026 18:39:51 +0000 Subject: [PATCH 3/6] fix(reports): align native metadata escaping and URL case --- .../mcp-app/src/threat-model-format.ts | 38 ++++++++++++++++--- .../mcp-app/tests/test_threat_model_render.ts | 36 +++++++++++++++++- .../scripts/report_projection.py | 2 +- .../tests/test_report_projection.py | 2 + 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/threat-model-format.ts b/plugins/codex-security/mcp-app/src/threat-model-format.ts index c57a771963..fd108aba51 100644 --- a/plugins/codex-security/mcp-app/src/threat-model-format.ts +++ b/plugins/codex-security/mcp-app/src/threat-model-format.ts @@ -9,6 +9,37 @@ function collapse(value: string): string { return value.replace(whitespace, " ").replace(/^ | $/gu, ""); } +/** Match report_projection._escape_markdown without changing URL ampersands. */ +function escapeMarkdown(value: string, inlineCode = false): string { + const urls = [...value.matchAll(/(?:https?|ftp):\/\/|(www)\./giu)] + .filter((match) => { + const previous = value[match.index - 1] ?? ""; + return match[1] + ? previous === "" || + /[\p{White_Space}\u001c-\u001f*_~(]/u.test(previous) + : !/[A-Za-z]/u.test(previous); + }) + .map((match) => { + const length = value + .slice(match.index) + .search(/[\p{White_Space}\u001c-\u001f<>]/u); + return { + start: match.index, + end: length < 0 ? value.length : match.index + length, + }; + }); + const pattern = inlineCode + ? /(?&]/gu + : /[\\`*_\[\]<>&]/gu; + return value.replace(pattern, (match, offset: number) => + match.length > 1 || + (match === "&" && + urls.some(({ start, end }) => start <= offset && offset < end)) + ? match + : "\\" + match, + ); +} + /** Plain metadata remains text; authored single-backtick spans remain intact. */ export function text(value: unknown, fallback = ""): string { const candidate = @@ -20,10 +51,5 @@ export function text(value: unknown, fallback = ""): string { ) ? `Text: ${normalized}` : normalized; - return protectedText - .split(/((? - index % 2 ? part : part.replace(/([\\`*_\[\]<>])/gu, "\\$1"), - ) - .join(""); + return escapeMarkdown(protectedText, true); } diff --git a/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts b/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts index 7c189f4e5e..ebbe59d764 100644 --- a/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts +++ b/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts @@ -109,6 +109,38 @@ equivalent( }, }, ); +equivalent( + "footer entities remain literal in prose and scope paths", + { + ...markdownModel, + origin: "provided", + scope: { + includePaths: [ + "docs/<tag>.md", + "docs/www.example.com/<tag>.md", + "docs/`<tag>`.md", + "docs/a,<tag>.md", + ], + excludePaths: ["tmp/&.md"], + summary: + "R&D decodes <tag> and &lt;. Keep `&` literal.", + }, + }, + { target: "R&D <workspace>" }, +); +for (const url of [ + "https://example.com/?first=1&second=2", + "http://example.com/?payload=<tag>&mode=1", + "ftp://example.com/?first=1&second=2", + "www.example.com/?payload=<tag>&mode=1", + "WWW.example.com/?first=1&second=2", + "WwW.example.com/?first=1&second=2", + "https://example.com/`a`?first=1&second=2", +]) { + equivalent(`footer URL ampersands ${url}`, markdownModel, { + target: `${url} then & and \`https://example.com/?a=1&b=2 &\`.`, + }); +} for (const ending of ["", "\n", "\n\n", "\r\n"]) { equivalent(`authored Markdown ending ${JSON.stringify(ending)}`, { ...markdownModel, @@ -180,10 +212,10 @@ test("Python finalizer recognizes native renderer bytes as the current projectio status: "running", threatModel, target: { - displayName: "Example [workspace]", + displayName: "R&D <workspace>", revision: "example-revision", }, - scope: { includePaths: ["src"], excludePaths: [] }, + scope: { includePaths: ["src", "docs/<tag>.md"], excludePaths: [] }, }; const markdown = renderThreatModel({ threatModel, diff --git a/plugins/codex-security/scripts/report_projection.py b/plugins/codex-security/scripts/report_projection.py index 69b362b906..72e3d90018 100644 --- a/plugins/codex-security/scripts/report_projection.py +++ b/plugins/codex-security/scripts/report_projection.py @@ -21,7 +21,7 @@ } WRITEUP_REPORT_PATH_RE = re.compile(r"^findings/([a-z0-9][a-z0-9._-]*)/\1\.md$") INLINE_CODE_RE = r"(?]+" +AUTOLINK_RE = r"(?:(?]+" class ReportProjectionError(ValueError): diff --git a/plugins/codex-security/tests/test_report_projection.py b/plugins/codex-security/tests/test_report_projection.py index 10c32c01c9..74f2dfc3e6 100644 --- a/plugins/codex-security/tests/test_report_projection.py +++ b/plugins/codex-security/tests/test_report_projection.py @@ -820,6 +820,8 @@ def test_projection_keeps_html_entities_literal_in_markdown() -> None: "https://example.com/?first=1&second=2", "http://example.com/?payload=<tag>&mode=1", "www.example.com/?payload=<tag>&mode=1", + "WWW.example.com/?first=1&second=2", + "WwW.example.com/?first=1&second=2", "ftp://example.com/?first=1&second=2", "https://example.com/`a`?first=1&second=2", "www.example.com/`a`?first=1&second=2", From fdf38268f3e85f595ee13c49d04ec16bf1ade618 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 23 Oct 2026 19:41:26 +0000 Subject: [PATCH 4/6] fix(reports): preserve fuzzy links and email ampersands --- .../codex-security/mcp-app/src/threat-model-format.ts | 9 +++++++++ .../mcp-app/tests/test_threat_model_render.ts | 11 +++++++++++ plugins/codex-security/scripts/report_projection.py | 11 ++++++++++- .../codex-security/tests/test_report_projection.py | 10 ++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/plugins/codex-security/mcp-app/src/threat-model-format.ts b/plugins/codex-security/mcp-app/src/threat-model-format.ts index 4481ff1edd..3394cbf654 100644 --- a/plugins/codex-security/mcp-app/src/threat-model-format.ts +++ b/plugins/codex-security/mcp-app/src/threat-model-format.ts @@ -28,6 +28,15 @@ function escapeMarkdown(value: string, inlineCode = false): string { end: length < 0 ? value.length : match.index + length, }; }); + // Conservatively retain fuzzy links without a TLD or Unicode domain table. + const domain = String.raw`[^\p{White_Space}\u001c-\u001f<>/@:?&.#\\]+(?:\.[^\p{White_Space}\u001c-\u001f<>/@:?&.#\\]+)+`; + const bareLink = new RegExp( + String.raw`(?/@]+@${domain})[^\p{White_Space}\u001c-\u001f<>]*`, + "gu", + ); + for (const match of value.matchAll(bareLink)) { + urls.push({ start: match.index, end: match.index + match[0].length }); + } const pattern = inlineCode ? /(?&]/gu : /[\\`*_\[\]<>&]/gu; diff --git a/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts b/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts index d2c4fd8093..a37275cd5c 100644 --- a/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts +++ b/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts @@ -136,6 +136,17 @@ for (const url of [ "WWW.example.com/?first=1&second=2", "WwW.example.com/?first=1&second=2", "https://example.com/`a`?first=1&second=2", + "example.com/?first=1&second=2", + "//example.com/?first=1&second=2", + '"example.com/?first=1&second=2"', + "[example.com/?first=1&second=2]", + "éxample.com/?first=1&second=2", + "例子.com/?first=1&second=2", + "example.com/?payload=<tag>&mode=1", + "example.com/?first=1&word;=2", + "//example.com/?first=1&word;=2", + "user&tag@example.com", + "mailto:user&tag@example.com", ]) { equivalent(`footer URL ampersands ${url}`, markdownModel, { target: `${url} then & and \`https://example.com/?a=1&b=2 &\`.`, diff --git a/plugins/codex-security/scripts/report_projection.py b/plugins/codex-security/scripts/report_projection.py index 26d61684a6..d88dcf82c4 100644 --- a/plugins/codex-security/scripts/report_projection.py +++ b/plugins/codex-security/scripts/report_projection.py @@ -22,6 +22,11 @@ WRITEUP_REPORT_PATH_RE = re.compile(r"^findings/([a-z0-9][a-z0-9._-]*)/\1\.md$") INLINE_CODE_RE = r"(?]+" +LINK_DOMAIN_RE = r"[^\s<>/@:?&.#\\]+(?:\.[^\s<>/@:?&.#\\]+)+" +BARE_LINK_RE = ( + rf"(?/@]+@{LINK_DOMAIN_RE})[^\s<>]*" +) class ReportProjectionError(ValueError): @@ -30,7 +35,11 @@ class ReportProjectionError(ValueError): def _escape_markdown(value: str, *, inline_code: bool = False) -> str: # Locate whole URLs before matching code spans: a URL can contain backticks. - urls = [match.span() for match in re.finditer(AUTOLINK_RE, value)] + urls = [ + match.span() + for pattern in (AUTOLINK_RE, BARE_LINK_RE) + for match in re.finditer(pattern, value) + ] punctuation = r"[\\`*_\[\]<>&]" pattern = f"(?P{INLINE_CODE_RE})|{punctuation}" if inline_code else punctuation diff --git a/plugins/codex-security/tests/test_report_projection.py b/plugins/codex-security/tests/test_report_projection.py index 237bb46511..157eaf4d68 100644 --- a/plugins/codex-security/tests/test_report_projection.py +++ b/plugins/codex-security/tests/test_report_projection.py @@ -825,6 +825,16 @@ def test_projection_keeps_html_entities_literal_in_markdown() -> None: "ftp://example.com/?first=1&second=2", "https://example.com/`a`?first=1&second=2", "www.example.com/`a`?first=1&second=2", + "example.com/?first=1&second=2", + "//example.com/?first=1&second=2", + '"example.com/?first=1&second=2"', + "éxample.com/?first=1&second=2", + "例子.com/?first=1&second=2", + "example.com/?payload=<tag>&mode=1", + "example.com/?first=1&word;=2", + "//example.com/?first=1&word;=2", + "user&tag@example.com", + "mailto:user&tag@example.com", ], ) def test_projection_preserves_bare_url_ampersands_in_prose_and_tables(url: str) -> None: From 74ecc2400b47de73dcd7bbaedfbfaf1f8a57b4ee Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 23 Oct 2026 19:52:49 +0000 Subject: [PATCH 5/6] fix(reports): separate literal paths from prose links --- .../mcp-app/src/threat-model-format.ts | 53 +++++++++++-------- .../mcp-app/tests/test_threat_model_render.ts | 14 +++++ .../scripts/report_projection.py | 35 ++++++------ .../tests/test_report_projection.py | 24 +++++++-- 4 files changed, 81 insertions(+), 45 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/threat-model-format.ts b/plugins/codex-security/mcp-app/src/threat-model-format.ts index 3394cbf654..bfc2170e4d 100644 --- a/plugins/codex-security/mcp-app/src/threat-model-format.ts +++ b/plugins/codex-security/mcp-app/src/threat-model-format.ts @@ -1,4 +1,5 @@ const whitespace = /[\p{White_Space}\u001c-\u001f]+/gu; +const entityAmp = /&(?=(?:#[0-9]+|#[xX][0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]*);)/u; /** Match the retained artifact projection's Python whitespace semantics. */ export function nonblank(value: string): boolean { @@ -10,7 +11,7 @@ function collapse(value: string): string { } /** Match report_projection._escape_markdown without changing URL ampersands. */ -function escapeMarkdown(value: string, inlineCode = false): string { +function escapeMarkdown(value: string): string { const urls = [...value.matchAll(/(?:https?|ftp):\/\/|(www)\./giu)] .filter((match) => { const previous = value[match.index - 1] ?? ""; @@ -28,18 +29,21 @@ function escapeMarkdown(value: string, inlineCode = false): string { end: length < 0 ? value.length : match.index + length, }; }); - // Conservatively retain fuzzy links without a TLD or Unicode domain table. - const domain = String.raw`[^\p{White_Space}\u001c-\u001f<>/@:?&.#\\]+(?:\.[^\p{White_Space}\u001c-\u001f<>/@:?&.#\\]+)+`; - const bareLink = new RegExp( - String.raw`(?/@]+@${domain})[^\p{White_Space}\u001c-\u001f<>]*`, + // Preserve protocol-relative and fuzzy links without a domain/TLD table. + for (const match of value.matchAll(/[^\p{White_Space}\u001c-\u001f<>]+/gu)) { + const token = match[0]; + const dot = token.split("/", 1)[0]!.indexOf("."); + if ( + token.includes("//") || + (dot >= 0 && /[/?#]/u.test(token.slice(dot + 1))) + ) { + urls.push({ start: match.index, end: match.index + token.length }); + } + } + const pattern = new RegExp( + /(?]/u.source + "|" + entityAmp.source, "gu", ); - for (const match of value.matchAll(bareLink)) { - urls.push({ start: match.index, end: match.index + match[0].length }); - } - const pattern = inlineCode - ? /(?&]/gu - : /[\\`*_\[\]<>&]/gu; return value.replace(pattern, (match, offset: number) => match.length > 1 || (match === "&" && @@ -51,25 +55,28 @@ function escapeMarkdown(value: string, inlineCode = false): string { /** Match report_projection._path_text with ensure_ascii for quoted scope paths. */ export function pathText(value: string): string { - if ( - !value.includes(",") && - value === collapse(value) && - !/[\u0000-\u001f]/u.test(value) - ) - return escapeMarkdown(value); + const quoted = + value.includes(",") || + value !== collapse(value) || + /[\u0000-\u001f]/u.test(value); + if (!quoted && !entityAmp.test(value)) + return value.replace(/([\\`*_\[\]<>])/gu, "\\$1"); // Escape UTF-16 units so quoted paths do not depend on Unicode versions. - const label = JSON.stringify(value).replace( - /[\u007f-\uffff]/g, - (unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, "0")}`, - ); + const label = quoted + ? JSON.stringify(value).replace( + /[\u007f-\uffff]/g, + (unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, "0")}`, + ) + : value; const marker = "`".repeat( (label.match(/`+/gu) ?? []).reduce( (length, run) => Math.max(length, run.length + 1), 1, ), ); - return `${marker}${label}${marker}`; + const padding = label.startsWith("`") || label.endsWith("`") ? " " : ""; + return `${marker}${padding}${label}${padding}${marker}`; } /** Plain metadata remains text; authored single-backtick spans remain intact. */ @@ -83,5 +90,5 @@ export function text(value: unknown, fallback = ""): string { ) ? `Text: ${normalized}` : normalized; - return escapeMarkdown(protectedText, true); + return escapeMarkdown(protectedText); } diff --git a/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts b/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts index a37275cd5c..d002553fbd 100644 --- a/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts +++ b/plugins/codex-security/mcp-app/tests/test_threat_model_render.ts @@ -120,6 +120,12 @@ equivalent( "docs/www.example.com/<tag>.md", "docs/`<tag>`.md", "docs/a,<tag>.md", + "dir<tag>.example/file.md", + "dir.example/file<tag>.md", + "www.example.com/file&.md", + "`&`", + "`&.md", + "file&`", ], excludePaths: ["tmp/&.md"], summary: @@ -147,6 +153,10 @@ for (const url of [ "//example.com/?first=1&word;=2", "user&tag@example.com", "mailto:user&tag@example.com", + "//localhost:8080/?first=1&word;=2", + "{//example.com/?first=1&word;=2}", + "?example.com/?first=1&word;=2", + "mailto:user&tag@localhost", ]) { equivalent(`footer URL ampersands ${url}`, markdownModel, { target: `${url} then & and \`https://example.com/?a=1&b=2 &\`.`, @@ -275,6 +285,10 @@ test("Python finalizer recognizes native renderer bytes as the current projectio "\u{1fae9},notes.md", "docs/<tag>.md", "docs/a,<tag>.md", + "dir<tag>.example/file.md", + "dir.example/file<tag>.md", + "www.example.com/file&.md", + "`&`", ], excludePaths: [" "], }; diff --git a/plugins/codex-security/scripts/report_projection.py b/plugins/codex-security/scripts/report_projection.py index d88dcf82c4..61c8876e10 100644 --- a/plugins/codex-security/scripts/report_projection.py +++ b/plugins/codex-security/scripts/report_projection.py @@ -21,32 +21,30 @@ } WRITEUP_REPORT_PATH_RE = re.compile(r"^findings/([a-z0-9][a-z0-9._-]*)/\1\.md$") INLINE_CODE_RE = r"(?]+" -LINK_DOMAIN_RE = r"[^\s<>/@:?&.#\\]+(?:\.[^\s<>/@:?&.#\\]+)+" -BARE_LINK_RE = ( - rf"(?/@]+@{LINK_DOMAIN_RE})[^\s<>]*" -) class ReportProjectionError(ValueError): """Raised when a canonical scan cannot be projected into a valid report.""" -def _escape_markdown(value: str, *, inline_code: bool = False) -> str: +def _escape_markdown(value: str) -> str: # Locate whole URLs before matching code spans: a URL can contain backticks. - urls = [ - match.span() - for pattern in (AUTOLINK_RE, BARE_LINK_RE) - for match in re.finditer(pattern, value) - ] - punctuation = r"[\\`*_\[\]<>&]" - pattern = f"(?P{INLINE_CODE_RE})|{punctuation}" if inline_code else punctuation + urls = [match.span() for match in re.finditer(AUTOLINK_RE, value)] + # Preserve protocol-relative and fuzzy links without a domain/TLD table. + for match in re.finditer(r"[^\s<>]+", value): + token = match[0] + dot = token.split("/", 1)[0].find(".") + if "//" in token or (dot >= 0 and re.search(r"[/?#]", token[dot + 1 :])): + urls.append(match.span()) + punctuation = r"[\\`*_\[\]<>]|" + ENTITY_AMP_RE + pattern = f"(?P{INLINE_CODE_RE})|{punctuation}" def escape(match: re.Match[str]) -> str: if match.lastgroup == "code": return match[0] - # GFM autolinks keep backslashes literally, including before query separators. + # GFM autolinks retain backslashes literally, including in query strings. if match[0] == "&" and any(start <= match.start() < end for start, end in urls): return "&" return "\\" + match[0] @@ -59,7 +57,7 @@ def _text(value: Any, fallback: str) -> str: normalized = " ".join(candidate.split()) if re.match(r"^(?:#{1,6}\s|[-*+]\s|>\s|```|~~~|\d+[.)]\s|\|)", normalized): normalized = f"Text: {normalized}" - return _escape_markdown(normalized, inline_code=True) + return _escape_markdown(normalized) def _strings(value: Any) -> list[str]: @@ -79,10 +77,11 @@ def _path_label(value: str, *, ensure_ascii: bool = False) -> str: def _path_text(value: str, *, ensure_ascii: bool = False) -> str: label = _path_label(value, ensure_ascii=ensure_ascii) - if label != value: + if label != value or re.search(ENTITY_AMP_RE, value): marker = _code_fence(label, minimum=1) - return f"{marker}{label}{marker}" - return _escape_markdown(value) + padding = " " if label.startswith("`") or label.endswith("`") else "" + return f"{marker}{padding}{label}{padding}{marker}" + return re.sub(r"([\\`*_\[\]<>])", r"\\\1", value) def _cell(value: Any) -> str: diff --git a/plugins/codex-security/tests/test_report_projection.py b/plugins/codex-security/tests/test_report_projection.py index 157eaf4d68..f50dea2786 100644 --- a/plugins/codex-security/tests/test_report_projection.py +++ b/plugins/codex-security/tests/test_report_projection.py @@ -811,7 +811,7 @@ def test_projection_keeps_html_entities_literal_in_markdown() -> None: markdown_text = PROJECTION.generate_report_markdown(manifest, findings, coverage).decode() assert r"Sanitizer decodes \<script\> and \&lt;" in markdown_text - assert r"- Included paths: docs/\<b\>.md" in markdown_text + assert "- Included paths: `docs/<b>.md`" in markdown_text @pytest.mark.parametrize( @@ -835,6 +835,10 @@ def test_projection_keeps_html_entities_literal_in_markdown() -> None: "//example.com/?first=1&word;=2", "user&tag@example.com", "mailto:user&tag@example.com", + "//localhost:8080/?first=1&word;=2", + "{//example.com/?first=1&word;=2}", + "?example.com/?first=1&word;=2", + "mailto:user&tag@localhost", ], ) def test_projection_preserves_bare_url_ampersands_in_prose_and_tables(url: str) -> None: @@ -847,7 +851,7 @@ def test_projection_preserves_bare_url_ampersands_in_prose_and_tables(url: str) assert f"Reproduce with {url} then compare \\&." in markdown assert f"| Confidence rationale | Request: {url} |" in markdown - assert r"R\&D decoded \<tag\>." in markdown + assert r"R&D decoded \<tag\>." in markdown def test_projection_preserves_inline_code_and_existing_url_escapes() -> None: @@ -860,6 +864,12 @@ def test_projection_preserves_inline_code_and_existing_url_escapes() -> None: coverage["includePaths"] = [ "docs/www.example.com/<tag>.md", "docs/`<tag>`.md", + "dir<tag>.example/file.md", + "dir.example/file<tag>.md", + "www.example.com/file&.md", + "`&`", + "`&.md", + "file&`", ] markdown = PROJECTION.generate_report_markdown(manifest, findings, coverage).decode() @@ -867,8 +877,14 @@ def test_projection_preserves_inline_code_and_existing_url_escapes() -> None: assert "Compare `&` and `https://example.com/?a=1&b=2 &`" in markdown assert r"https://example.com/a\_b?first=1&second=2 and \&." in markdown assert "Keep https://example.com/`a _b_` as written." in markdown - assert r"- Included paths: docs/www.example.com/\<tag\>.md" in markdown - assert r"docs/\`\<tag\>\`.md" in markdown + assert "- Included paths: `docs/www.example.com/<tag>.md`" in markdown + assert "``docs/`<tag>`.md``" in markdown + assert "`dir<tag>.example/file.md`" in markdown + assert "`dir.example/file<tag>.md`" in markdown + assert "`www.example.com/file&.md`" in markdown + assert "`` `&` ``" in markdown + assert "`` `&.md ``" in markdown + assert "`` file&` ``" in markdown def test_projection_omits_informational_findings() -> None: From 3da6e6378df23d31d1d6399a2599ede89b907f87 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 23 Oct 2026 20:00:35 +0000 Subject: [PATCH 6/6] refactor(reports): remove redundant URL matching --- .../mcp-app/src/threat-model-format.ts | 25 ++++++------------- .../scripts/report_projection.py | 4 +-- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/threat-model-format.ts b/plugins/codex-security/mcp-app/src/threat-model-format.ts index bfc2170e4d..d333a4ae57 100644 --- a/plugins/codex-security/mcp-app/src/threat-model-format.ts +++ b/plugins/codex-security/mcp-app/src/threat-model-format.ts @@ -12,23 +12,14 @@ function collapse(value: string): string { /** Match report_projection._escape_markdown without changing URL ampersands. */ function escapeMarkdown(value: string): string { - const urls = [...value.matchAll(/(?:https?|ftp):\/\/|(www)\./giu)] - .filter((match) => { - const previous = value[match.index - 1] ?? ""; - return match[1] - ? previous === "" || - /[\p{White_Space}\u001c-\u001f*_~(]/u.test(previous) - : !/[A-Za-z]/u.test(previous); - }) - .map((match) => { - const length = value - .slice(match.index) - .search(/[\p{White_Space}\u001c-\u001f<>]/u); - return { - start: match.index, - end: length < 0 ? value.length : match.index + length, - }; - }); + const urls = [ + ...value.matchAll( + /(?]+/giu, + ), + ].map((match) => ({ + start: match.index, + end: match.index + match[0].length, + })); // Preserve protocol-relative and fuzzy links without a domain/TLD table. for (const match of value.matchAll(/[^\p{White_Space}\u001c-\u001f<>]+/gu)) { const token = match[0]; diff --git a/plugins/codex-security/scripts/report_projection.py b/plugins/codex-security/scripts/report_projection.py index 61c8876e10..86eb38fd08 100644 --- a/plugins/codex-security/scripts/report_projection.py +++ b/plugins/codex-security/scripts/report_projection.py @@ -22,7 +22,7 @@ WRITEUP_REPORT_PATH_RE = re.compile(r"^findings/([a-z0-9][a-z0-9._-]*)/\1\.md$") INLINE_CODE_RE = r"(?]+" +WWW_LINK_RE = r"(?]+" class ReportProjectionError(ValueError): @@ -31,7 +31,7 @@ class ReportProjectionError(ValueError): def _escape_markdown(value: str) -> str: # Locate whole URLs before matching code spans: a URL can contain backticks. - urls = [match.span() for match in re.finditer(AUTOLINK_RE, value)] + urls = [match.span() for match in re.finditer(WWW_LINK_RE, value)] # Preserve protocol-relative and fuzzy links without a domain/TLD table. for match in re.finditer(r"[^\s<>]+", value): token = match[0]