Skip to content

Commit 4cc6955

Browse files
committed
update
1 parent 0411fe6 commit 4cc6955

1 file changed

Lines changed: 131 additions & 38 deletions

File tree

src/codeaudit/ci_workflowscan.py

Lines changed: 131 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,17 @@
3333

3434

3535
def 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

9691
def 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}\tweakness: {validation}\tseverity:{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

Comments
 (0)