From 40880f8fdb0987a16e67a6b133d4c95b1366bd89 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira Date: Sat, 31 Oct 2026 02:17:56 +0000 Subject: [PATCH] fix(ci): strip free-form text and sanitize metadata on skipped JUnit elements --- .github/scripts/verify_sanitized_artifacts.py | 25 ++++++++--- tests/test_verify_sanitized_artifacts.py | 44 +++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/.github/scripts/verify_sanitized_artifacts.py b/.github/scripts/verify_sanitized_artifacts.py index be177b5e5..cd2959b1f 100755 --- a/.github/scripts/verify_sanitized_artifacts.py +++ b/.github/scripts/verify_sanitized_artifacts.py @@ -57,6 +57,8 @@ ALLOWED_ATTRIBUTES_BY_TAG = { "skipped": frozenset({"message", "type"}), } +SAFE_SKIPPED_STATUSES = frozenset({"skipped", "todo", "ignored", "disabled", "pending", "omitted", "test skipped"}) + SENSITIVE_KEY_PATTERN = re.compile( r"(?i)(password|passwd|secret|token|api_key|apikey|access_token|refresh_token|" r"private_key|auth_token|client_secret|session|session_id|sid|csrf|xsrf|jwt|credential|authorization)" @@ -186,6 +188,15 @@ def prune_and_sanitize_junit_element(elem: ET.Element) -> bool: if attr_lower not in allowed_attrs: del elem.attrib[attr_name] modified = True + elif tag_lower == "skipped": + val = elem.attrib[attr_name].strip() + if val.lower() in SAFE_SKIPPED_STATUSES: + if elem.attrib[attr_name] != val.lower(): + elem.attrib[attr_name] = val.lower() + modified = True + else: + elem.attrib[attr_name] = "skipped" + modified = True else: val = elem.attrib[attr_name] if isinstance(val, str): @@ -194,9 +205,9 @@ def prune_and_sanitize_junit_element(elem: ET.Element) -> bool: elem.attrib[attr_name] = new_val modified = True - # 3. Handle text content: structural elements do not carry free-form text - if tag_lower in ("testsuites", "testsuite", "testcase"): - if elem.text and elem.text.strip(): + # 3. Handle text content: structural and skipped elements do not carry free-form text + if tag_lower in ("testsuites", "testsuite", "testcase", "skipped"): + if elem.text: elem.text = None modified = True elif tag_lower in ("failure", "error"): @@ -206,11 +217,11 @@ def prune_and_sanitize_junit_element(elem: ET.Element) -> bool: elem.text = new_text modified = True - # Clean up tails + # 4. Clean up tails: strip any non-whitespace characters from tails to prevent leaks if elem.tail and elem.tail.strip(): - new_tail, changed = sanitize_text_content(elem.tail) - if changed: - elem.tail = new_tail + cleaned_tail = re.sub(r"\S+", "", elem.tail) + if cleaned_tail != elem.tail: + elem.tail = cleaned_tail modified = True return modified diff --git a/tests/test_verify_sanitized_artifacts.py b/tests/test_verify_sanitized_artifacts.py index 488873437..17dfb0098 100644 --- a/tests/test_verify_sanitized_artifacts.py +++ b/tests/test_verify_sanitized_artifacts.py @@ -250,6 +250,50 @@ def test_standard_node_and_playwright_reports_remain_valid_and_retain_identities assert "session_token" not in pw_text +def test_skipped_element_content_tail_and_attributes_leak_prevented(tmp_path): + """Regression: PRIVATE_VALUE_123, attributes, and tails cannot leak sensitive values.""" + results_dir = tmp_path / "test-results" + results_dir.mkdir() + junit = results_dir / "browser-junit.xml" + xml_with_skipped = """ + + + PRIVATE_VALUE_123PRIVATE_VALUE_123 + + +""" + junit.write_text(xml_with_skipped) + + ok = verify_and_sanitize_artifacts(str(results_dir)) + assert ok is True + assert junit.exists() + + sanitized_xml = junit.read_text() + # Synthetic sensitive values that do not match secret regexes must be absent + assert "PRIVATE_VALUE_123" not in sanitized_xml + assert "UNOFFICIAL_SECRET_456" not in sanitized_xml + + # The output must remain valid JUnit XML + tree = ET.parse(junit) + root = tree.getroot() + assert root.tag == "testsuites" + cases = list(root.iter("testcase")) + assert len(cases) == 1 + + # Testcase identity remains intact + testcase = cases[0] + assert testcase.get("name") == "skipped-case" + assert testcase.get("classname") == "sample" + + # Skipped element remains present and classified as skipped + skipped_elems = list(testcase.iter("skipped")) + assert len(skipped_elems) == 1 + skipped_elem = skipped_elems[0] + assert skipped_elem.text is None or not skipped_elem.text.strip() + assert skipped_elem.get("message") == "skipped" + assert skipped_elem.get("type") == "skipped" + + def test_unrecognized_xml_purged(tmp_path): """Negative control: XML reports that are not recognized JUnit reports are purged.""" results_dir = tmp_path / "test-results"