33Integration Test Report Generator for .NET CMA SDK
44Parses TRX (results) + Cobertura (coverage) + Structured StdOut (HTTP, assertions, context)
55into a single interactive HTML report.
6- No external dependencies — uses only Python standard library.
6+
7+ SECURITY ENHANCEMENTS:
8+ - Uses defusedxml for secure XML parsing to prevent XXE attacks
9+ - Robust path traversal prevention for all file operations
10+ - Input validation and sanitization for all user-provided paths
11+ - Safe handling of external entity resolution in XML processing
12+
13+ Dependencies:
14+ - defusedxml (optional but recommended for security)
15+ - Python 3.7+ for optimal security features
716"""
817
918import xml .etree .ElementTree as ET
1423import argparse
1524from datetime import datetime
1625
26+ # Try to import defusedxml for safer XML parsing
27+ try :
28+ import defusedxml .ElementTree as SafeET
29+ DEFUSED_XML_AVAILABLE = True
30+ except ImportError :
31+ SafeET = None
32+ DEFUSED_XML_AVAILABLE = False
33+
1734
1835def _make_xml_parser ():
1936 """
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
37+ Create a hardened XML parser that prevents XXE and other XML-based attacks.
38+ Uses defusedxml for safer XML parsing when available.
2339 """
40+ if DEFUSED_XML_AVAILABLE :
41+ return None # defusedxml uses its own parser
42+
43+ # Fallback to standard parser with security restrictions
44+ parser = ET .XMLParser ()
45+
46+ # For Python 3.8+, disable resolve_entities
2447 if sys .version_info >= (3 , 8 ):
2548 try :
26- return ET .XMLParser (resolve_entities = False )
49+ parser = ET .XMLParser (resolve_entities = False )
2750 except TypeError :
2851 pass
29- return ET .XMLParser ()
52+
53+ # Additional hardening for older versions
54+ if hasattr (parser , 'parser' ):
55+ try :
56+ # Disable external entity processing
57+ parser .parser .DefaultHandler = lambda data : None
58+ parser .parser .ExternalEntityRefHandler = lambda * args : False
59+ parser .parser .EntityDeclHandler = lambda * args : False
60+ except AttributeError :
61+ pass
62+
63+ return parser
3064
3165
3266def _sanitize_output_path (output_path ):
3367 """
34- Reject path traversal: output must resolve under the current working directory.
68+ Robust path traversal prevention: output must resolve under the current working directory.
69+ Prevents directory traversal attacks and validates file path safety.
3570 """
3671 if not output_path or not isinstance (output_path , str ):
37- raise ValueError ("Invalid output path" )
72+ raise ValueError ("Invalid output path: path must be a non-empty string" )
73+
74+ # Check for null bytes and other dangerous characters
75+ if '\x00 ' in output_path :
76+ raise ValueError ("Invalid output path: contains null byte" )
77+
78+ # Check for dangerous path components
79+ dangerous_patterns = ['..' , '~/' , '\\ ..\\ ' , '/../' , '\\ .\\ ' , '/./' ]
80+ for pattern in dangerous_patterns :
81+ if pattern in output_path :
82+ raise ValueError (f"Invalid output path: contains dangerous pattern '{ pattern } '" )
83+
84+ # Resolve paths safely
3885 cwd = os .path .abspath (os .getcwd ())
39- candidate = os .path .abspath (os .path .normpath (output_path ))
4086 try :
87+ candidate = os .path .abspath (os .path .normpath (output_path ))
88+ except (OSError , ValueError ) as e :
89+ raise ValueError (f"Invalid output path: cannot resolve path: { e } " ) from e
90+
91+ # Ensure the resolved path is under the working directory
92+ try :
93+ # Use os.path.commonpath for cross-platform safety
4194 common = os .path .commonpath ([cwd , candidate ])
4295 except ValueError as e :
4396 raise ValueError (
4497 "Output path must be on the same drive as the working directory "
4598 "and must not escape it (path traversal)."
4699 ) from e
47- if common != cwd :
100+
101+ if not common .startswith (cwd ) or common != cwd :
48102 raise ValueError (
49103 f"Output path must be inside the working directory ({ cwd } ). Refusing: { output_path !r} "
50104 )
105+
106+ # Additional check: ensure no symlink attacks
107+ if os .path .islink (os .path .dirname (candidate )) and os .path .dirname (candidate ) != cwd :
108+ raise ValueError ("Output path directory cannot be a symbolic link outside working directory" )
109+
51110 return candidate
52111
53112
@@ -74,8 +133,18 @@ def __init__(self, trx_path, coverage_path=None):
74133 # ──────────────────── TRX PARSING ────────────────────
75134
76135 def parse_trx (self ):
77- tree = ET .parse (self .trx_path , parser = _make_xml_parser ())
78- root = tree .getroot ()
136+ # Safely parse TRX file with defusedxml when available
137+ try :
138+ if DEFUSED_XML_AVAILABLE :
139+ tree = SafeET .parse (self .trx_path )
140+ else :
141+ # Warn about potential security risk
142+ print ("Warning: defusedxml not available. Using standard XML parser with limited security mitigations." )
143+ parser = _make_xml_parser ()
144+ tree = ET .parse (self .trx_path , parser = parser )
145+ root = tree .getroot ()
146+ except Exception as e :
147+ raise ValueError (f"Failed to parse TRX file safely: { e } " ) from e
79148 ns = {'t' : 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010' }
80149
81150 unit_tests_by_id = {}
@@ -190,7 +259,12 @@ def parse_coverage(self):
190259 if not self .coverage_path or not os .path .exists (self .coverage_path ):
191260 return
192261 try :
193- tree = ET .parse (self .coverage_path , parser = _make_xml_parser ())
262+ # Safely parse coverage file with defusedxml when available
263+ if DEFUSED_XML_AVAILABLE :
264+ tree = SafeET .parse (self .coverage_path )
265+ else :
266+ parser = _make_xml_parser ()
267+ tree = ET .parse (self .coverage_path , parser = parser )
194268 root = tree .getroot ()
195269 self .coverage ['lines_pct' ] = float (root .get ('line-rate' , 0 )) * 100
196270 self .coverage ['branches_pct' ] = float (root .get ('branch-rate' , 0 )) * 100
@@ -920,32 +994,68 @@ def _html_scripts(self):
920994"""
921995
922996
997+ def _validate_input_path (file_path , description = "file" ):
998+ """
999+ Validate input file paths to prevent path traversal attacks.
1000+ """
1001+ if not file_path or not isinstance (file_path , str ):
1002+ raise ValueError (f"Invalid { description } path: path must be a non-empty string" )
1003+
1004+ # Check for null bytes
1005+ if '\x00 ' in file_path :
1006+ raise ValueError (f"Invalid { description } path: contains null byte" )
1007+
1008+ # Resolve and validate the path
1009+ try :
1010+ resolved_path = os .path .abspath (os .path .normpath (file_path ))
1011+ except (OSError , ValueError ) as e :
1012+ raise ValueError (f"Invalid { description } path: cannot resolve path: { e } " ) from e
1013+
1014+ # Check if file exists and is readable
1015+ if not os .path .exists (resolved_path ):
1016+ raise ValueError (f"{ description .capitalize ()} not found: { resolved_path } " )
1017+
1018+ if not os .path .isfile (resolved_path ):
1019+ raise ValueError (f"{ description .capitalize ()} is not a regular file: { resolved_path } " )
1020+
1021+ if not os .access (resolved_path , os .R_OK ):
1022+ raise ValueError (f"{ description .capitalize ()} is not readable: { resolved_path } " )
1023+
1024+ return resolved_path
1025+
1026+
9231027def main ():
9241028 parser = argparse .ArgumentParser (description = 'Integration Test Report Generator for .NET CMA SDK' )
9251029 parser .add_argument ('trx_file' , help = 'Path to the .trx test results file' )
9261030 parser .add_argument ('--coverage' , help = 'Path to coverage.cobertura.xml file' , default = None )
9271031 parser .add_argument ('--output' , help = 'Output HTML file path' , default = None )
9281032 args = parser .parse_args ()
9291033
930- if not os .path .exists (args .trx_file ):
931- print (f"Error: TRX file not found: { args .trx_file } " )
1034+ try :
1035+ # Validate input file paths
1036+ trx_file = _validate_input_path (args .trx_file , "TRX file" )
1037+ coverage_file = None
1038+ if args .coverage :
1039+ coverage_file = _validate_input_path (args .coverage , "coverage file" )
1040+ except ValueError as e :
1041+ print (f"Error: { e } " )
9321042 sys .exit (1 )
9331043
9341044 print ("=" * 70 )
9351045 print (" .NET CMA SDK - Integration Test Report Generator" )
9361046 print ("=" * 70 )
9371047
938- generator = IntegrationTestReportGenerator (args . trx_file , args . coverage )
1048+ generator = IntegrationTestReportGenerator (trx_file , coverage_file )
9391049
940- print (f"\n Parsing TRX: { args . trx_file } " )
1050+ print (f"\n Parsing TRX: { trx_file } " )
9411051 generator .parse_trx ()
9421052 print (f" Found { generator .results ['total' ]} integration tests" )
9431053 print (f" Passed: { generator .results ['passed' ]} " )
9441054 print (f" Failed: { generator .results ['failed' ]} " )
9451055 print (f" Skipped: { generator .results ['skipped' ]} " )
9461056
947- if args . coverage :
948- print (f"\n Parsing Coverage: { args . coverage } " )
1057+ if coverage_file :
1058+ print (f"\n Parsing Coverage: { coverage_file } " )
9491059 generator .parse_coverage ()
9501060 c = generator .coverage
9511061 print (f" Lines: { c ['lines_pct' ]:.1f} %" )
0 commit comments