1515from 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+
1854class 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"\n Generating 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
923971if __name__ == "__main__" :
0 commit comments