3333
3434
3535def ci_scan (input_path , output = "text" , nosec = True ):
36- """Basic SAST scan to be used in CI workflows
36+ """Run a SAST scan for CI workflows.
3737
38- The nosec is set to true for CI workflows by default, it can be changed.
39- Security weakness SHOULD be marked for an exit 0 status in your CI.
38+ Args:
39+ input_path: Path to the file or directory to scan.
40+ output: Report format ("text", "html", or "json").
41+ nosec: Whether to ignore findings marked with ``# nosec``.
4042
41- Note: If you use JSON output you will have an exit status 0, since you have to
42- determine yourself if there are weaknesses found in your code.
43-
44- Set an option in your CI job like e.g. allow_failure: true since jobs that run
45- can result in detecting weaknesses and this is no failure of the job!
43+ Exits:
44+ 0: No reportable weaknesses found and used always for JSON output
45+ 3: Weaknesses found.
46+ 1: Scan error or invalid output format.
4647 """
4748 try :
4849 scanresult = filescan (input_path , nosec = nosec )
@@ -79,31 +80,32 @@ def ci_scan(input_path, output="text", nosec=True):
7980 sys .exit (1 )
8081
8182
82- def report_result_json (scanresult ):
83- """Returns scan result in json outputformat.
84- Note: not (yet) directly usable since you still need to dive in the dict structure to retrieve results, if any for weaknesses found per file. The resulting json structure is outlined in the documentation. You can use e.g. the `jq` tool. Or join the Python Code Audit community to create CI json output that suites your needs!
85- Note that it is hierarchical json structure. See the docs!
86- """
87- if not isinstance (scanresult , dict ):
88- raise TypeError ("Expected scanresult to be a dictionary" )
89- file_security_info = scanresult .get ("file_security_info" )
90- files_with_findings_count = 0
91- # Add brackets and parse
92- json_text = json .dumps (file_security_info , indent = 4 )
93- return json_text , files_with_findings_count
83+ def safe_line (x ):
84+ """Safe sorting helper function"""
85+ try :
86+ return int (x .get ("line" , 0 ))
87+ except (TypeError , ValueError ):
88+ return 0
9489
9590
9691def report_result_txt (scanresult ):
97- """Returns scan result in txt output format."""
92+ """Generate a human-readable text report for CI mode scan results.
93+
94+ Args:
95+ scanresult: Dictionary returned by the scan engine.
96+
97+ Returns:
98+ Tuple[str, int]: Report text and number of files with findings.
99+ """
98100 # Ensure scanresult is a dictionary to prevent crash on .get()
99101 if not isinstance (scanresult , dict ):
100102 print ("❌ Error: Invalid scan result data format structure." , file = sys .stderr )
101- return ""
103+ return "" , 0
102104
103105 file_security_info = scanresult .get ("file_security_info" )
104106 if not isinstance (file_security_info , dict ) or len (file_security_info ) == 0 :
105107 print ("⚠️ Warning: No file security info found!" , file = sys .stderr )
106- return ""
108+ return "" , 0
107109
108110 output = ""
109111 files_with_findings_count = 0
@@ -136,13 +138,6 @@ def report_result_txt(scanresult):
136138 file_scan_location = file_info .get ("FilePath" , "Unknown" )
137139 output += f"File location: { file_scan_location } \n "
138140
139- # --- Safe sorting ---
140- def safe_line (x ):
141- try :
142- return int (x .get ("line" , 0 ))
143- except (TypeError , ValueError ):
144- return 0
145-
146141 sorted_findings = sorted (all_findings , key = safe_line )
147142
148143 for finding in sorted_findings :
@@ -158,7 +153,7 @@ def safe_line(x):
158153 f"line:{ line } \t weakness: { validation } \t severity:{ severity } ->{ info } \n "
159154 )
160155
161- # Gather stats
156+ # Gather sast results that are relevant for CI output
162157 stats = scanresult .get ("statistics_overview" )
163158 if not isinstance (stats , dict ):
164159 stats = {}
@@ -255,13 +250,6 @@ def report_result_html(scanresult):
255250 <tbody>
256251 """
257252
258- # --- Safe sorting ---
259- def safe_line (x ):
260- try :
261- return int (x .get ("line" , 0 ))
262- except (TypeError , ValueError ):
263- return 0
264-
265253 sorted_findings = sorted (all_findings , key = safe_line )
266254
267255 for finding in sorted_findings :
@@ -289,3 +277,108 @@ def safe_line(x):
289277 html += "</div>"
290278 html += DISCLAIMER_TEXT + FOOTER_TEXT
291279 return html , 1
280+
281+
282+ def report_result_json (scanresult ):
283+ """Returns scan result in JSON output format as tuple (json_string, files_with_findings_count).
284+ By design no codesnippet is returned in this json output.
285+ """
286+ # Ensure scanresult is a dictionary to prevent crash on .get()
287+ if not isinstance (scanresult , dict ):
288+ print ("❌ Error: Invalid scan result data format structure." , file = sys .stderr )
289+ error_json = json .dumps (
290+ {"error" : True , "message" : "Invalid scan result data format structure." }
291+ )
292+ return error_json , 0
293+
294+ file_security_info = scanresult .get ("file_security_info" )
295+ if not isinstance (file_security_info , dict ) or len (file_security_info ) == 0 :
296+ print ("⚠️ Warning: No file security info found!" , file = sys .stderr )
297+ warning_json = json .dumps (
298+ {"warning" : True , "message" : "No file security info found!" }
299+ )
300+ return warning_json , 0
301+
302+ # Prepare data structure for JSON output
303+ files_data = []
304+ files_with_findings_count = 0
305+
306+ for file_info in file_security_info .values ():
307+ if not isinstance (file_info , dict ):
308+ continue
309+
310+ sast_result = file_info .get ("sast_result" )
311+ if not isinstance (sast_result , dict ) or len (sast_result ) == 0 :
312+ continue
313+
314+ # --- Normalize findings ---
315+ all_findings = []
316+ for v in sast_result .values ():
317+ if isinstance (v , dict ):
318+ all_findings .append (v )
319+ elif isinstance (v , list ):
320+ all_findings .extend ([item for item in v if isinstance (item , dict )])
321+
322+ if not all_findings :
323+ continue
324+
325+ # If we made it here, this file actually has valid findings
326+ files_with_findings_count += 1
327+ filename = file_info .get ("FileName" , "Unknown File" )
328+ file_scan_location = file_info .get ("FilePath" , "Unknown" )
329+
330+ sorted_findings = sorted (all_findings , key = safe_line )
331+
332+ # Format findings for this file
333+ findings_list = []
334+ for finding in sorted_findings :
335+ if not isinstance (finding , dict ):
336+ continue
337+
338+ finding_entry = {
339+ "line" : finding .get ("line" , "—" ),
340+ "weakness" : finding .get ("validation" , "—" ),
341+ "severity" : finding .get ("severity" , "—" ),
342+ "info" : finding .get ("info" , "—" ),
343+ }
344+ findings_list .append (finding_entry )
345+
346+ # Add file data
347+ file_data = {
348+ "filename" : filename ,
349+ "file_location" : file_scan_location ,
350+ "num_issues" : len (all_findings ),
351+ "findings" : findings_list ,
352+ }
353+ files_data .append (file_data )
354+
355+ # Gather stats
356+ stats = scanresult .get ("statistics_overview" )
357+ if not isinstance (stats , dict ):
358+ stats = {}
359+ total_number_of_files = stats .get ("Number_Of_Files" , 1 )
360+
361+ # Build the output structure
362+ output_data = {
363+ "files_data" : files_data ,
364+ "total_files_with_findings" : files_with_findings_count ,
365+ "total_files_checked" : total_number_of_files ,
366+ }
367+
368+ # Build the summary structure
369+ if files_with_findings_count == 0 :
370+ summary_data = {
371+ "status" : "clean" ,
372+ "message" : "✅ No security issue(s) found in file(s) or Package." ,
373+ "total_files_with_findings" : files_with_findings_count ,
374+ "total_files_checked" : total_number_of_files ,
375+ }
376+ summary_json = json .dumps (summary_data , indent = 2 )
377+ return summary_json , files_with_findings_count
378+ else :
379+ # For consistency, include the summary in the output data
380+ output_data ["summary" ] = (
381+ f"Total files with findings: { files_with_findings_count } of { total_number_of_files } Python files checked."
382+ )
383+ output_json = json .dumps (output_data , indent = 2 )
384+ return output_json , files_with_findings_count
0 commit comments