mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-01 02:32:21 +01:00
fix(ci): strip free-form text and sanitize metadata on skipped JUnit elements
This commit is contained in:
@@ -57,6 +57,8 @@ ALLOWED_ATTRIBUTES_BY_TAG = {
|
|||||||
"skipped": frozenset({"message", "type"}),
|
"skipped": frozenset({"message", "type"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SAFE_SKIPPED_STATUSES = frozenset({"skipped", "todo", "ignored", "disabled", "pending", "omitted", "test skipped"})
|
||||||
|
|
||||||
SENSITIVE_KEY_PATTERN = re.compile(
|
SENSITIVE_KEY_PATTERN = re.compile(
|
||||||
r"(?i)(password|passwd|secret|token|api_key|apikey|access_token|refresh_token|"
|
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)"
|
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:
|
if attr_lower not in allowed_attrs:
|
||||||
del elem.attrib[attr_name]
|
del elem.attrib[attr_name]
|
||||||
modified = True
|
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:
|
else:
|
||||||
val = elem.attrib[attr_name]
|
val = elem.attrib[attr_name]
|
||||||
if isinstance(val, str):
|
if isinstance(val, str):
|
||||||
@@ -194,9 +205,9 @@ def prune_and_sanitize_junit_element(elem: ET.Element) -> bool:
|
|||||||
elem.attrib[attr_name] = new_val
|
elem.attrib[attr_name] = new_val
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
# 3. Handle text content: structural elements do not carry free-form text
|
# 3. Handle text content: structural and skipped elements do not carry free-form text
|
||||||
if tag_lower in ("testsuites", "testsuite", "testcase"):
|
if tag_lower in ("testsuites", "testsuite", "testcase", "skipped"):
|
||||||
if elem.text and elem.text.strip():
|
if elem.text:
|
||||||
elem.text = None
|
elem.text = None
|
||||||
modified = True
|
modified = True
|
||||||
elif tag_lower in ("failure", "error"):
|
elif tag_lower in ("failure", "error"):
|
||||||
@@ -206,11 +217,11 @@ def prune_and_sanitize_junit_element(elem: ET.Element) -> bool:
|
|||||||
elem.text = new_text
|
elem.text = new_text
|
||||||
modified = True
|
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():
|
if elem.tail and elem.tail.strip():
|
||||||
new_tail, changed = sanitize_text_content(elem.tail)
|
cleaned_tail = re.sub(r"\S+", "", elem.tail)
|
||||||
if changed:
|
if cleaned_tail != elem.tail:
|
||||||
elem.tail = new_tail
|
elem.tail = cleaned_tail
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
return modified
|
return modified
|
||||||
|
|||||||
@@ -250,6 +250,50 @@ def test_standard_node_and_playwright_reports_remain_valid_and_retain_identities
|
|||||||
assert "session_token" not in pw_text
|
assert "session_token" not in pw_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_skipped_element_content_tail_and_attributes_leak_prevented(tmp_path):
|
||||||
|
"""Regression: <skipped>PRIVATE_VALUE_123</skipped>, 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 = """<testsuites>
|
||||||
|
<testsuite name="sample" tests="1" skipped="1">
|
||||||
|
<testcase name="skipped-case" classname="sample">
|
||||||
|
<skipped message="PRIVATE_VALUE_123" type="UNOFFICIAL_SECRET_456">PRIVATE_VALUE_123</skipped>PRIVATE_VALUE_123
|
||||||
|
</testcase>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>"""
|
||||||
|
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):
|
def test_unrecognized_xml_purged(tmp_path):
|
||||||
"""Negative control: XML reports that are not recognized JUnit reports are purged."""
|
"""Negative control: XML reports that are not recognized JUnit reports are purged."""
|
||||||
results_dir = tmp_path / "test-results"
|
results_dir = tmp_path / "test-results"
|
||||||
|
|||||||
Reference in New Issue
Block a user