55import re
66import xml .etree .ElementTree as ET
77
8- def validate_workflow (workflow_file , console , source_dir = None ):
8+ def validate_workflow (workflow_file , source_dir , console ):
99 workflow_path = Path (workflow_file )
10+ source_root = (workflow_path .parent / source_dir )
1011
1112 console .print (f"[cyan]Validating:[/cyan] { workflow_path .name } " )
1213 console .print ()
@@ -15,31 +16,35 @@ def validate_workflow(workflow_file, console, source_dir=None):
1516 warnings = []
1617 info = []
1718
19+ def finalize ():
20+ show_results (console , errors , warnings , info )
21+ return len (errors ) == 0
22+
1823 try :
1924 with open (workflow_path , 'r' ) as f :
2025 content = f .read ()
2126
2227 if not content .strip ():
2328 errors .append ("File is empty" )
24- return show_results ( console , errors , warnings , info )
29+ return finalize ( )
2530
2631 # strict XML syntax check
2732 try :
2833 ET .fromstring (content )
2934 except ET .ParseError as e :
3035 errors .append (f"Invalid XML: { str (e )} " )
31- return show_results ( console , errors , warnings , info )
36+ return finalize ( )
3237
3338 try :
3439 soup = BeautifulSoup (content , 'xml' )
3540 except Exception as e :
3641 errors .append (f"Invalid XML: { str (e )} " )
37- return show_results ( console , errors , warnings , info )
42+ return finalize ( )
3843
3944 root = soup .find ('graphml' )
4045 if not root :
4146 errors .append ("Not a valid GraphML file - missing <graphml> root element" )
42- return show_results ( console , errors , warnings , info )
47+ return finalize ( )
4348
4449 # check the graph attributes
4550 graph = soup .find ('graph' )
@@ -65,6 +70,9 @@ def validate_workflow(workflow_file, console, source_dir=None):
6570 else :
6671 info .append (f"Found { len (edges )} edge(s)" )
6772
73+ if not source_root .exists ():
74+ warnings .append (f"Source directory not found: { source_root } " )
75+
6876 node_labels = []
6977 for node in nodes :
7078 #check the node id
@@ -84,6 +92,11 @@ def validate_workflow(workflow_file, console, source_dir=None):
8492 label = label_tag .text .strip ()
8593 node_labels .append (label )
8694
95+ # reject shell metacharacters to prevent command injection (#251)
96+ if re .search (r'[;&|`$\'"()\\]' , label ):
97+ errors .append (f"Node '{ label } ' contains unsafe shell characters" )
98+ continue
99+
87100 if ':' not in label :
88101 warnings .append (f"Node '{ label } ' missing format 'ID:filename'" )
89102 else :
@@ -96,6 +109,10 @@ def validate_workflow(workflow_file, console, source_dir=None):
96109 errors .append (f"Node '{ label } ' has no filename" )
97110 elif not any (filename .endswith (ext ) for ext in ['.py' , '.cpp' , '.m' , '.v' , '.java' ]):
98111 warnings .append (f"Node '{ label } ' has unusual file extension" )
112+ elif source_root .exists ():
113+ file_path = source_root / filename
114+ if not file_path .exists ():
115+ errors .append (f"Missing source file: { filename } " )
99116 else :
100117 warnings .append (f"Node { node_id } has no label" )
101118 except Exception as e :
@@ -138,44 +155,17 @@ def validate_workflow(workflow_file, console, source_dir=None):
138155 if file_edges > 0 :
139156 info .append (f"File-based edges: { file_edges } " )
140157
141- if source_dir :
142- _check_source_files (soup , Path (source_dir ), errors , warnings )
143-
144158 _check_cycles (soup , errors , warnings )
145159 _check_zmq_ports (soup , errors , warnings )
146160
147- show_results ( console , errors , warnings , info )
161+ return finalize ( )
148162
149163 except FileNotFoundError :
150164 console .print (f"[red]Error:[/red] File not found: { workflow_path } " )
165+ return False
151166 except Exception as e :
152167 console .print (f"[red]Validation failed:[/red] { str (e )} " )
153-
154- def _check_source_files (soup , source_path , errors , warnings ):
155- nodes = soup .find_all ('node' )
156-
157- for node in nodes :
158- label_tag = node .find ('y:NodeLabel' ) or node .find ('NodeLabel' )
159- if not label_tag or not label_tag .text :
160- continue
161-
162- label = label_tag .text .strip ()
163- if ':' not in label :
164- warnings .append (f"Skipping node with invalid label format (expected 'ID:filename')" )
165- continue
166-
167- parts = label .split (':' )
168- if len (parts ) != 2 :
169- warnings .append (f"Skipping node '{ label } ' with invalid format" )
170- continue
171-
172- _ , filename = parts
173- if not filename :
174- continue
175-
176- file_path = source_path / filename
177- if not file_path .exists ():
178- errors .append (f"Source file not found: { filename } " )
168+ return False
179169
180170def _check_cycles (soup , errors , warnings ):
181171 nodes = soup .find_all ('node' )
0 commit comments