Skip to content
65 changes: 49 additions & 16 deletions plugins/codex-security/mcp-app/src/threat-model-format.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -9,27 +10,64 @@ function collapse(value: string): string {
return value.replace(whitespace, " ").replace(/^ | $/gu, "");
}

/** Match report_projection._escape_markdown without changing URL ampersands. */
function escapeMarkdown(value: string): string {
const urls = [
...value.matchAll(
/(?<![^\p{White_Space}\u001c-\u001f*_~(])www\.[^\p{White_Space}\u001c-\u001f<>]+/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];
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(
/(?<!`)`[^`\n]+`(?!`)|[\\`*_\[\]<>]/u.source + "|" + entityAmp.source,
"gu",
);
return value.replace(pattern, (match, offset: number) =>
match.length > 1 ||
(match === "&" &&
urls.some(({ start, end }) => start <= offset && offset < end))
? match
: "\\" + match,
);
}

/** 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)
)
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. */
Expand All @@ -43,10 +81,5 @@ export function text(value: unknown, fallback = ""): string {
)
? `Text: ${normalized}`
: normalized;
return protectedText
.split(/((?<!`)`[^`\n]+`(?!`))/u)
.map((part, index) =>
index % 2 ? part : part.replace(/([\\`*_\[\]<>])/gu, "\\$1"),
)
.join("");
return escapeMarkdown(protectedText);
}
68 changes: 66 additions & 2 deletions plugins/codex-security/mcp-app/tests/test_threat_model_render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,59 @@ equivalent(
},
},
);
equivalent(
"footer entities remain literal in prose and scope paths",
{
...markdownModel,
origin: "provided",
scope: {
includePaths: [
"docs/&lt;tag&gt;.md",
"docs/www.example.com/&lt;tag&gt;.md",
"docs/`&lt;tag&gt;`.md",
"docs/a,&lt;tag&gt;.md",
"dir&lt;tag&gt;.example/file.md",
"dir.example/file&lt;tag&gt;.md",
"www.example.com/file&amp;.md",
"`&amp;`",
"`&amp;.md",
"file&amp;`",
],
excludePaths: ["tmp/&amp;.md"],
summary:
"R&D decodes &#x3C;tag&#x3E; and &amp;lt;. Keep `&amp;` literal.",
},
},
{ target: "R&D &lt;workspace&gt;" },
);
for (const url of [
"https://example.com/?first=1&second=2",
"http://example.com/?payload=&#x3C;tag&#x3E;&mode=1",
"ftp://example.com/?first=1&second=2",
"www.example.com/?payload=&lt;tag&gt;&mode=1",
"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=&lt;tag&gt;&mode=1",
"example.com/?first=1&word;=2",
"//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 &amp; and \`https://example.com/?a=1&b=2 &amp;\`.`,
});
}
for (const [paths, expected] of [
[
[
Expand Down Expand Up @@ -225,7 +278,18 @@ test("non-object models report a threatModel diagnostic", () => {

test("Python finalizer recognizes native renderer bytes as the current projection", () => {
const scope = {
includePaths: ["src", "a,b.ts", "a b", "\u{1fae9},notes.md"],
includePaths: [
"src",
"a,b.ts",
"a b",
"\u{1fae9},notes.md",
"docs/&lt;tag&gt;.md",
"docs/a,&lt;tag&gt;.md",
"dir&lt;tag&gt;.example/file.md",
"dir.example/file&lt;tag&gt;.md",
"www.example.com/file&amp;.md",
"`&amp;`",
],
excludePaths: [" "],
};
const threatModel = { summary: "Saved model", assets: ["Queue"], scope };
Expand All @@ -234,7 +298,7 @@ test("Python finalizer recognizes native renderer bytes as the current projectio
status: "running",
threatModel,
target: {
displayName: "Example [workspace]",
displayName: "R&D &lt;workspace&gt;",
revision: "example-revision",
},
scope,
Expand Down
36 changes: 30 additions & 6 deletions plugins/codex-security/scripts/report_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,44 @@
"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"(?<!`)`[^`\n]+`(?!`)"
ENTITY_AMP_RE = r"&(?=(?:#[0-9]+|#[xX][0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]*);)"
WWW_LINK_RE = r"(?<![^\s*_~(])(?i:www)\.[^\s<>]+"


class ReportProjectionError(ValueError):
"""Raised when a canonical scan cannot be projected into a valid report."""


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(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]
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<code>{INLINE_CODE_RE})|{punctuation}"

def escape(match: re.Match[str]) -> str:
if match.lastgroup == "code":
return match[0]
# 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]

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"((?<!`)`[^`\n]+`(?!`))", normalized))
)
return _escape_markdown(normalized)


def _strings(value: Any) -> list[str]:
Expand All @@ -54,9 +77,10 @@ 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}"
padding = " " if label.startswith("`") or label.endswith("`") else ""
return f"{marker}{padding}{label}{padding}{marker}"
return re.sub(r"([\\`*_\[\]<>])", r"\\\1", value)


Expand Down
84 changes: 84 additions & 0 deletions plugins/codex-security/tests/test_report_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,90 @@ def test_projection_escapes_raw_html_in_markdown() -> None:
assert r'\<img src="x" onerror="alert(1)"\>' in markdown_text


def test_projection_keeps_html_entities_literal_in_markdown() -> None:
manifest, findings, coverage = canonical_documents()
findings["findings"][0]["summary"] = "Sanitizer decodes &#x3C;script&#x3E; and &amp;lt;"
coverage["includePaths"] = ["docs/&lt;b&gt;.md"]

markdown_text = PROJECTION.generate_report_markdown(manifest, findings, coverage).decode()

assert r"Sanitizer decodes \&#x3C;script\&#x3E; and \&amp;lt;" in markdown_text
assert "- Included paths: `docs/&lt;b&gt;.md`" in markdown_text


@pytest.mark.parametrize(
"url",
[
"https://example.com/?first=1&second=2",
"http://example.com/?payload=&#x3C;tag&#x3E;&mode=1",
"www.example.com/?payload=&lt;tag&gt;&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",
"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=&lt;tag&gt;&mode=1",
"example.com/?first=1&word;=2",
"//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:
manifest, findings, coverage = canonical_documents()
finding = findings["findings"][0]
finding["summary"] = f"R&D decoded &#60;tag&#62;. Reproduce with {url} then compare &amp;."
finding["confidence"]["rationale"] = f"Request: {url}"

markdown = PROJECTION.generate_report_markdown(manifest, findings, coverage).decode()

assert f"Reproduce with {url} then compare \\&amp;." in markdown
assert f"| Confidence rationale | Request: {url} |" in markdown
assert r"R&D decoded \&#60;tag\&#62;." in markdown


def test_projection_preserves_inline_code_and_existing_url_escapes() -> None:
manifest, findings, coverage = canonical_documents()
findings["findings"][0]["summary"] = (
"Compare `&amp;` and `https://example.com/?a=1&b=2 &amp;` with "
"https://example.com/a_b?first=1&second=2 and &amp;. "
"Keep https://example.com/`a _b_` as written."
)
coverage["includePaths"] = [
"docs/www.example.com/&lt;tag&gt;.md",
"docs/`&lt;tag&gt;`.md",
"dir&lt;tag&gt;.example/file.md",
"dir.example/file&lt;tag&gt;.md",
"www.example.com/file&amp;.md",
"`&amp;`",
"`&amp;.md",
"file&amp;`",
]

markdown = PROJECTION.generate_report_markdown(manifest, findings, coverage).decode()

assert "Compare `&amp;` and `https://example.com/?a=1&b=2 &amp;`" in markdown
assert r"https://example.com/a\_b?first=1&second=2 and \&amp;." in markdown
assert "Keep https://example.com/`a _b_` as written." in markdown
assert "- Included paths: `docs/www.example.com/&lt;tag&gt;.md`" in markdown
assert "``docs/`&lt;tag&gt;`.md``" in markdown
assert "`dir&lt;tag&gt;.example/file.md`" in markdown
assert "`dir.example/file&lt;tag&gt;.md`" in markdown
assert "`www.example.com/file&amp;.md`" in markdown
assert "`` `&amp;` ``" in markdown
assert "`` `&amp;.md ``" in markdown
assert "`` file&amp;` ``" in markdown


def test_projection_omits_informational_findings() -> None:
manifest, findings, coverage = canonical_documents()
findings["findings"][0]["severity"]["level"] = "informational"
Expand Down
Loading