Skip to content

Commit 7bdbef6

Browse files
obarreraOrlando Barrera II
andauthored
Added SARIF support to the CLI (#43)
Co-authored-by: Orlando Barrera II <[email protected]>
1 parent 8b04089 commit 7bdbef6

File tree

2 files changed

+160
-2
lines changed

2 files changed

+160
-2
lines changed

socketsecurity/core/messages.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import os
23

34
from mdutils import MdUtils
45
from socketsecurity.core.classes import Diff, Purl, Issue
@@ -7,6 +8,128 @@
78

89
class Messages:
910

11+
@staticmethod
12+
def map_severity_to_sarif(severity: str) -> str:
13+
"""
14+
Map Socket severity levels to SARIF levels (GitHub code scanning).
15+
"""
16+
severity_mapping = {
17+
"low": "note",
18+
"medium": "warning",
19+
"middle": "warning", # older data might say "middle"
20+
"high": "error",
21+
"critical": "error",
22+
}
23+
return severity_mapping.get(severity.lower(), "note")
24+
25+
26+
@staticmethod
27+
def find_line_in_file(pkg_name: str, manifest_file: str) -> tuple[int, str]:
28+
"""
29+
Search 'manifest_file' for 'pkg_name'.
30+
Return (line_number, line_content) if found, else (1, fallback).
31+
"""
32+
if not manifest_file or not os.path.isfile(manifest_file):
33+
return 1, f"[No {manifest_file or 'manifest'} found in repo]"
34+
try:
35+
with open(manifest_file, "r", encoding="utf-8") as f:
36+
lines = f.readlines()
37+
for i, line in enumerate(lines, start=1):
38+
if pkg_name.lower() in line.lower():
39+
return i, line.rstrip("\n")
40+
except Exception as e:
41+
return 1, f"[Error reading {manifest_file}: {e}]"
42+
return 1, f"[Package '{pkg_name}' not found in {manifest_file}]"
43+
44+
@staticmethod
45+
def create_security_comment_sarif(diff: Diff) -> dict:
46+
"""
47+
Create SARIF-compliant output from the diff report.
48+
"""
49+
scan_failed = False
50+
if len(diff.new_alerts) == 0:
51+
for alert in diff.new_alerts:
52+
alert: Issue
53+
if alert.error:
54+
scan_failed = True
55+
break
56+
57+
# Basic SARIF structure
58+
sarif_data = {
59+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
60+
"version": "2.1.0",
61+
"runs": [
62+
{
63+
"tool": {
64+
"driver": {
65+
"name": "Socket Security",
66+
"informationUri": "https://socket.dev",
67+
"rules": []
68+
}
69+
},
70+
"results": []
71+
}
72+
]
73+
}
74+
75+
rules_map = {}
76+
results_list = []
77+
78+
for alert in diff.new_alerts:
79+
alert: Issue
80+
pkg_name = alert.pkg_name
81+
pkg_version = alert.pkg_version
82+
rule_id = f"{pkg_name}=={pkg_version}"
83+
severity = alert.severity
84+
85+
# Title and descriptions
86+
title = f"Alert generated for {pkg_name}=={pkg_version} by Socket Security"
87+
full_desc = f"{alert.title} - {alert.description}"
88+
short_desc = f"{alert.props.get('note', '')}\r\n\r\nSuggested Action:\r\n{alert.suggestion}"
89+
90+
# Find the manifest file and line details
91+
introduced_list = alert.introduced_by
92+
if introduced_list and isinstance(introduced_list[0], list) and len(introduced_list[0]) > 1:
93+
manifest_file = introduced_list[0][1]
94+
else:
95+
manifest_file = alert.manifests or "requirements.txt"
96+
97+
line_number, line_content = Messages.find_line_in_file(pkg_name, manifest_file)
98+
99+
# Define the rule if not already defined
100+
if rule_id not in rules_map:
101+
rules_map[rule_id] = {
102+
"id": rule_id,
103+
"name": f"{pkg_name}=={pkg_version}",
104+
"shortDescription": {"text": title},
105+
"fullDescription": {"text": full_desc},
106+
"helpUri": alert.url,
107+
"defaultConfiguration": {"level": Messages.map_severity_to_sarif(severity)},
108+
}
109+
110+
# Add the result
111+
result_obj = {
112+
"ruleId": rule_id,
113+
"message": {"text": short_desc},
114+
"locations": [
115+
{
116+
"physicalLocation": {
117+
"artifactLocation": {"uri": manifest_file},
118+
"region": {
119+
"startLine": line_number,
120+
"snippet": {"text": line_content},
121+
},
122+
}
123+
}
124+
],
125+
}
126+
results_list.append(result_obj)
127+
128+
sarif_data["runs"][0]["tool"]["driver"]["rules"] = list(rules_map.values())
129+
sarif_data["runs"][0]["results"] = results_list
130+
131+
return sarif_data
132+
10133
@staticmethod
11134
def create_security_comment_json(diff: Diff) -> dict:
12135
scan_failed = False

socketsecurity/socketcli.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,14 @@
170170
type=float
171171
)
172172

173+
parser.add_argument(
174+
'--enable-sarif',
175+
help='Enable SARIF output of results instead of table or JSON format',
176+
action='store_true',
177+
default=False
178+
)
179+
180+
173181
def output_console_comments(diff_report: Diff, sbom_file_name: str = None) -> None:
174182
if diff_report.id != "NO_DIFF_RAN":
175183
console_security_comment = Messages.create_console_security_alert_table(diff_report)
@@ -188,6 +196,25 @@ def output_console_comments(diff_report: Diff, sbom_file_name: str = None) -> No
188196
else:
189197
log.info("No New Security issues detected by Socket Security")
190198

199+
def output_console_sarif(diff_report: Diff, sbom_file_name: str = None) -> None:
200+
"""
201+
Generate SARIF output from the diff report and save it to a file.
202+
"""
203+
if diff_report.id != "NO_DIFF_RAN":
204+
# Generate the SARIF structure using Messages
205+
console_security_comment = Messages.create_security_comment_sarif(diff_report)
206+
207+
# Save the SARIF output to the specified SBOM file name or fallback to a default
208+
save_sbom_file(diff_report, sbom_file_name)
209+
# Print the SARIF output to the console in JSON format
210+
print(json.dumps(console_security_comment, indent=2))
211+
212+
# Handle exit codes based on alert severity
213+
if not report_pass(diff_report) and not blocking_disabled:
214+
sys.exit(1)
215+
elif len(diff_report.new_alerts) > 0 and not blocking_disabled:
216+
# Warning alerts without blocking
217+
sys.exit(5)
191218

192219
def output_console_json(diff_report: Diff, sbom_file_name: str = None) -> None:
193220
if diff_report.id != "NO_DIFF_RAN":
@@ -257,6 +284,7 @@ def main_code():
257284
sbom_file = arguments.sbom_file
258285
license_mode = arguments.generate_license
259286
enable_json = arguments.enable_json
287+
enable_sarif = arguments.enable_sarif
260288
disable_overview = arguments.disable_overview
261289
disable_security_issue = arguments.disable_security_issue
262290
ignore_commit_files = arguments.ignore_commit_files
@@ -401,7 +429,10 @@ def main_code():
401429
else:
402430
log.info("Starting non-PR/MR flow")
403431
diff = core.create_new_diff(target_path, params, workspace=target_path, no_change=no_change)
404-
if enable_json:
432+
if enable_sarif:
433+
log.debug("Outputting SARIF Results")
434+
output_console_sarif(diff, sbom_file)
435+
elif enable_json:
405436
log.debug("Outputting JSON Results")
406437
output_console_json(diff, sbom_file)
407438
else:
@@ -410,7 +441,11 @@ def main_code():
410441
log.info("API Mode")
411442
diff: Diff
412443
diff = core.create_new_diff(target_path, params, workspace=target_path, no_change=no_change)
413-
if enable_json:
444+
if enable_sarif:
445+
log.debug("Outputting SARIF Results")
446+
output_console_sarif(diff, sbom_file)
447+
elif enable_json:
448+
log.debug("Outputting JSON Results")
414449
output_console_json(diff, sbom_file)
415450
else:
416451
output_console_comments(diff, sbom_file)

0 commit comments

Comments
 (0)