Skip to content
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Python style
- F-strings in exceptions: `raise ValueError(f"Error {var}")`
- Google-style docstrings
- Single blank line at end of file
- No documentation for `__init__` methods
- No documentation for `__init__` methods and test modules

Patterns
- Classes with `__init__` cannot throw exceptions
Expand Down
24 changes: 23 additions & 1 deletion src/core/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,21 @@
# limitations under the License.
#

"""Pure utility functions – date helpers, hashing, and path normalisation."""
"""Pure utility functions"""

import hashlib
import re
from datetime import datetime, timezone

# Matches lines that start with 1-6 '#' followed by a space
_HEADING_RE = re.compile(r"^(#{1,6}\s)", re.MULTILINE)
# Matches '>' at the start of a line (blockquote)
_BLOCKQUOTE_RE = re.compile(r"^(>)", re.MULTILINE)
# Matches horizontal rules: three or more -, *, or _ on a line by themselves
_HR_RE = re.compile(r"^([-*_]{3,})\s*$", re.MULTILINE)
# Matches '|' at the start of a line (table row).
_TABLE_RE = re.compile(r"^(\|)", re.MULTILINE)
Comment thread
tmikula-dev marked this conversation as resolved.


def utc_today() -> str:
"""Return today's date in UTC as an ISO-8601 string (``YYYY-MM-DD``)."""
Expand Down Expand Up @@ -50,3 +59,16 @@ def normalize_path(path: str | None) -> str:
p = p.lstrip("/")
p = re.sub(r"/+", "/", p)
return p


def sanitize_markdown(text: str) -> str:
"""Escape block-level Markdown so text renders as plain content in an issue body."""
if not text:
return text

sanitized = _HEADING_RE.sub(r"\\\1", text)
sanitized = _BLOCKQUOTE_RE.sub(r"\\\1", sanitized)
sanitized = _HR_RE.sub(r"\\\1", sanitized)
sanitized = _TABLE_RE.sub(r"\\\1", sanitized)

return sanitized
10 changes: 5 additions & 5 deletions src/security/issues/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from typing import Any

from core.helpers import iso_date
from core.helpers import iso_date, sanitize_markdown
from core.rendering import render_markdown_template

from security.constants import NOT_AVAILABLE, SECMETA_TYPE_PARENT
Expand Down Expand Up @@ -60,7 +60,7 @@ def alert_extra_data(alert: Alert) -> dict[str, Any]:
"impact": alert.rule_details.impact,
"likelihood": alert.rule_details.likelihood,
"confidence": alert.rule_details.confidence,
"remediation": alert.rule_details.remediation,
"remediation": sanitize_markdown(alert.rule_details.remediation),
"references": references,
}

Expand All @@ -87,7 +87,7 @@ def build_parent_template_values(alert: Alert, *, rule_id: str, severity: str) -
return {
"category": alert.metadata.rule_name or NOT_AVAILABLE,
"avd_id": alert.alert_details.vulnerability or rule_id,
"title": alert.metadata.rule_description or rule_id,
"title": sanitize_markdown(alert.metadata.rule_description or rule_id),
"severity": severity,
"published_date": iso_date(alert.rule_details.published_date or NOT_AVAILABLE),
"package_name": alert.rule_details.package_name,
Expand Down Expand Up @@ -135,7 +135,7 @@ def build_child_issue_body(alert: Alert) -> str:
vulnerability = alert.alert_details.vulnerability
avd_id = vulnerability if vulnerability.startswith("AVD-") else NOT_AVAILABLE

title = alert.metadata.rule_description or alert.metadata.rule_id
title = sanitize_markdown(alert.metadata.rule_description or alert.metadata.rule_id)

scm_file = alert.alert_details.scm_file
start_line = alert.metadata.start_line
Expand All @@ -151,7 +151,7 @@ def build_child_issue_body(alert: Alert) -> str:
file_display = file_name or NOT_AVAILABLE

alert_hash = alert.alert_details.alert_hash
message = alert.alert_details.message
message = sanitize_markdown(alert.alert_details.message)

category = classify_category(alert)

Expand Down
73 changes: 73 additions & 0 deletions tests/core/test_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#
# Copyright 2026 ABSA Group Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import pytest

from core.helpers import sanitize_markdown


# sanitize_markdown


@pytest.mark.parametrize(
"text",
[None, "", "Normal text without markdown", "text with numbers 123"],
ids=["none", "empty", "plain", "numbers"],
)
def test_passthrough_unchanged(text: str | None) -> None:
assert sanitize_markdown(text) == text


@pytest.mark.parametrize(
"raw, expected",
[
("# Heading one", r"\# Heading one"),
("## Heading two", r"\## Heading two"),
("### Deep heading", r"\### Deep heading"),
("> quoted text", r"\> quoted text"),
("| col1 | col2 |", r"\| col1 | col2 |"),
("---", r"\---"),
],
ids=["h1", "h2", "h3", "blockquote", "table", "hr"],
)
def test_block_markdown_escaped(raw: str, expected: str) -> None:
assert expected == sanitize_markdown(raw)


@pytest.mark.parametrize(
"text",
["some **bold** text", "some __bold__ text", "use `code` here"],
ids=["bold", "underscore-bold", "backtick"],
)
def test_inline_markdown_preserved(text: str) -> None:
assert text == sanitize_markdown(text)


def test_heading_in_multiline() -> None:
text = "First line\n## Second is heading\nThird line"
result = sanitize_markdown(text)
assert r"\## Second is heading" in result
assert result.startswith("First line\n")


def test_real_aquasec_message() -> None:
msg = (
"## Black is the uncompromising Python code formatter. "
"Prior to 26.3.1, Black writes a cache file."
)
result = sanitize_markdown(msg)
assert not result.startswith("## ")
assert result.startswith(r"\## ")
Loading