Skip to content

Commit 2c0ee3d

Browse files
Fixed xml parser issue
1 parent 6be38ce commit 2c0ee3d

2 files changed

Lines changed: 89 additions & 10 deletions

File tree

Scripts/generate_integration_test_report.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,42 @@
1515
from datetime import datetime
1616

1717

18+
def _make_xml_parser():
19+
"""
20+
Harden ElementTree parsing against external entity resolution (XXE).
21+
resolve_entities=False is available on Python 3.8+; see:
22+
https://docs.python.org/3.10/library/xml.html#xml-vulnerabilities
23+
"""
24+
if sys.version_info >= (3, 8):
25+
try:
26+
return ET.XMLParser(resolve_entities=False)
27+
except TypeError:
28+
pass
29+
return ET.XMLParser()
30+
31+
32+
def _sanitize_output_path(output_path):
33+
"""
34+
Reject path traversal: output must resolve under the current working directory.
35+
"""
36+
if not output_path or not isinstance(output_path, str):
37+
raise ValueError("Invalid output path")
38+
cwd = os.path.abspath(os.getcwd())
39+
candidate = os.path.abspath(os.path.normpath(output_path))
40+
try:
41+
common = os.path.commonpath([cwd, candidate])
42+
except ValueError as e:
43+
raise ValueError(
44+
"Output path must be on the same drive as the working directory "
45+
"and must not escape it (path traversal)."
46+
) from e
47+
if common != cwd:
48+
raise ValueError(
49+
f"Output path must be inside the working directory ({cwd}). Refusing: {output_path!r}"
50+
)
51+
return candidate
52+
53+
1854
class IntegrationTestReportGenerator:
1955
def __init__(self, trx_path, coverage_path=None):
2056
self.trx_path = trx_path
@@ -38,10 +74,16 @@ def __init__(self, trx_path, coverage_path=None):
3874
# ──────────────────── TRX PARSING ────────────────────
3975

4076
def parse_trx(self):
41-
tree = ET.parse(self.trx_path)
77+
tree = ET.parse(self.trx_path, parser=_make_xml_parser())
4278
root = tree.getroot()
4379
ns = {'t': 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'}
4480

81+
unit_tests_by_id = {}
82+
for ut in root.findall('.//t:UnitTest', ns):
83+
tid = ut.get('id')
84+
if tid:
85+
unit_tests_by_id[tid] = ut
86+
4587
counters = root.find('.//t:ResultSummary/t:Counters', ns)
4688
if counters is not None:
4789
self.results['total'] = int(counters.get('total', 0))
@@ -82,7 +124,8 @@ def parse_trx(self):
82124
duration_str = result.get('duration', '0')
83125
duration = self._parse_duration(duration_str)
84126

85-
test_def = root.find(f".//t:UnitTest[@id='{test_id}']/t:TestMethod", ns)
127+
ut_el = unit_tests_by_id.get(test_id)
128+
test_def = ut_el.find('t:TestMethod', ns) if ut_el is not None else None
86129
class_name = test_def.get('className', '') if test_def is not None else ''
87130

88131
if 'IntegrationTest' not in class_name:
@@ -147,7 +190,7 @@ def parse_coverage(self):
147190
if not self.coverage_path or not os.path.exists(self.coverage_path):
148191
return
149192
try:
150-
tree = ET.parse(self.coverage_path)
193+
tree = ET.parse(self.coverage_path, parser=_make_xml_parser())
151194
root = tree.getroot()
152195
self.coverage['lines_pct'] = float(root.get('line-rate', 0)) * 100
153196
self.coverage['branches_pct'] = float(root.get('branch-rate', 0)) * 100
@@ -331,6 +374,7 @@ def _format_duration_display(self, seconds):
331374
# ──────────────────── HTML GENERATION ────────────────────
332375

333376
def generate_html(self, output_path):
377+
output_path = _sanitize_output_path(output_path)
334378
pass_rate = (self.results['passed'] / self.results['total'] * 100) if self.results['total'] > 0 else 0
335379
duration_display = self._format_duration_display(self.results['duration_seconds'])
336380

@@ -351,7 +395,7 @@ def generate_html(self, output_path):
351395

352396
with open(output_path, 'w', encoding='utf-8') as f:
353397
f.write(html)
354-
return output_path
398+
return os.path.abspath(output_path)
355399

356400
def _html_head(self):
357401
return f"""<!DOCTYPE html>
@@ -912,12 +956,16 @@ def main():
912956
output_file = args.output or f'integration-test-report_{timestamp}.html'
913957

914958
print(f"\nGenerating HTML report...")
915-
generator.generate_html(output_file)
959+
try:
960+
resolved_output = generator.generate_html(output_file)
961+
except ValueError as e:
962+
print(f"Error: {e}")
963+
sys.exit(1)
916964

917965
print(f"\n{'=' * 70}")
918-
print(f" Report generated: {os.path.abspath(output_file)}")
966+
print(f" Report generated: {resolved_output}")
919967
print(f"{'=' * 70}")
920-
print(f"\n open {os.path.abspath(output_file)}")
968+
print(f"\n open {resolved_output}")
921969

922970

923971
if __name__ == "__main__":

Scripts/run-integration-tests-with-report.sh

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,59 @@ echo "Project: $PROJECT_ROOT"
1414
echo "Run ID: $TIMESTAMP"
1515
echo ""
1616

17+
if ! command -v dotnet >/dev/null 2>&1; then
18+
echo "Error: 'dotnet' was not found on your PATH."
19+
echo "Install the .NET SDK (same major version as the test projects) and open a new terminal, or see:"
20+
echo " https://dotnet.microsoft.com/download"
21+
exit 1
22+
fi
23+
24+
if ! command -v python3 >/dev/null 2>&1; then
25+
echo "Error: 'python3' was not found on your PATH (required for the HTML report)."
26+
exit 1
27+
fi
28+
29+
SETTINGS="$PROJECT_ROOT/$TEST_PROJECT/appsettings.json"
30+
if [ ! -f "$SETTINGS" ]; then
31+
echo "Error: Integration tests require credentials at:"
32+
echo " $SETTINGS"
33+
echo "Copy the template and fill in real values (do not commit secrets):"
34+
echo " cp \"$PROJECT_ROOT/$TEST_PROJECT/appsettings.json.example\" \"$SETTINGS\""
35+
exit 1
36+
fi
37+
1738
# Step 1: Run ONLY integration tests, collect TRX + coverage
1839
TRX_FILE="IntegrationTest-Report-${TIMESTAMP}.trx"
40+
TRX_PATH="$PROJECT_ROOT/$TEST_PROJECT/TestResults/$TRX_FILE"
1941
echo "Step 1: Running integration tests..."
42+
set +e
2043
dotnet test "$PROJECT_ROOT/$TEST_PROJECT/$TEST_PROJECT.csproj" \
2144
--filter "FullyQualifiedName~IntegrationTest" \
2245
--logger "trx;LogFileName=$TRX_FILE" \
2346
--results-directory "$PROJECT_ROOT/$TEST_PROJECT/TestResults" \
2447
--collect:"XPlat code coverage" \
25-
--verbosity quiet || true
48+
--verbosity quiet
49+
TEST_EXIT=$?
50+
set -e
2651

2752
echo ""
28-
echo "Tests completed."
53+
echo "Tests completed (dotnet exit code: $TEST_EXIT)."
2954
echo ""
3055

56+
if [ ! -f "$TRX_PATH" ]; then
57+
echo "Error: TRX file was not created at:"
58+
echo " $TRX_PATH"
59+
echo "Fix the dotnet/test errors above (or install the SDK), then run this script again."
60+
exit 1
61+
fi
62+
3163
# Step 2: Locate the cobertura coverage file (most recent)
3264
COBERTURA=""
3365
if [ -d "$PROJECT_ROOT/$TEST_PROJECT/TestResults" ]; then
3466
COBERTURA=$(find "$PROJECT_ROOT/$TEST_PROJECT/TestResults" \
3567
-name "coverage.cobertura.xml" 2>/dev/null | sort -r | head -1)
3668
fi
3769

38-
TRX_PATH="$PROJECT_ROOT/$TEST_PROJECT/TestResults/$TRX_FILE"
3970
echo "TRX: $TRX_PATH"
4071
echo "Coverage: ${COBERTURA:-Not found}"
4172
echo ""

0 commit comments

Comments
 (0)