Skip to content

Added SARIF support to the CLI #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions socketsecurity/core/messages.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os

from mdutils import MdUtils
from socketsecurity.core.classes import Diff, Purl, Issue
Expand All @@ -7,6 +8,128 @@

class Messages:

@staticmethod
def map_severity_to_sarif(severity: str) -> str:
"""
Map Socket severity levels to SARIF levels (GitHub code scanning).
"""
severity_mapping = {
"low": "note",
"medium": "warning",
"middle": "warning", # older data might say "middle"
"high": "error",
"critical": "error",
}
return severity_mapping.get(severity.lower(), "note")


@staticmethod
def find_line_in_file(pkg_name: str, manifest_file: str) -> tuple[int, str]:
"""
Search 'manifest_file' for 'pkg_name'.
Return (line_number, line_content) if found, else (1, fallback).
"""
if not manifest_file or not os.path.isfile(manifest_file):
return 1, f"[No {manifest_file or 'manifest'} found in repo]"
try:
with open(manifest_file, "r", encoding="utf-8") as f:
lines = f.readlines()
for i, line in enumerate(lines, start=1):
if pkg_name.lower() in line.lower():
return i, line.rstrip("\n")
except Exception as e:
return 1, f"[Error reading {manifest_file}: {e}]"
return 1, f"[Package '{pkg_name}' not found in {manifest_file}]"

@staticmethod
def create_security_comment_sarif(diff: Diff) -> dict:
"""
Create SARIF-compliant output from the diff report.
"""
scan_failed = False
if len(diff.new_alerts) == 0:
for alert in diff.new_alerts:
alert: Issue
if alert.error:
scan_failed = True
break

# Basic SARIF structure
sarif_data = {
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "Socket Security",
"informationUri": "https://socket.dev",
"rules": []
}
},
"results": []
}
]
}

rules_map = {}
results_list = []

for alert in diff.new_alerts:
alert: Issue
pkg_name = alert.pkg_name
pkg_version = alert.pkg_version
rule_id = f"{pkg_name}=={pkg_version}"
severity = alert.severity

# Title and descriptions
title = f"Alert generated for {pkg_name}=={pkg_version} by Socket Security"
full_desc = f"{alert.title} - {alert.description}"
short_desc = f"{alert.props.get('note', '')}\r\n\r\nSuggested Action:\r\n{alert.suggestion}"

# Find the manifest file and line details
introduced_list = alert.introduced_by
if introduced_list and isinstance(introduced_list[0], list) and len(introduced_list[0]) > 1:
manifest_file = introduced_list[0][1]
else:
manifest_file = alert.manifests or "requirements.txt"

line_number, line_content = Messages.find_line_in_file(pkg_name, manifest_file)

# Define the rule if not already defined
if rule_id not in rules_map:
rules_map[rule_id] = {
"id": rule_id,
"name": f"{pkg_name}=={pkg_version}",
"shortDescription": {"text": title},
"fullDescription": {"text": full_desc},
"helpUri": alert.url,
"defaultConfiguration": {"level": Messages.map_severity_to_sarif(severity)},
}

# Add the result
result_obj = {
"ruleId": rule_id,
"message": {"text": short_desc},
"locations": [
{
"physicalLocation": {
"artifactLocation": {"uri": manifest_file},
"region": {
"startLine": line_number,
"snippet": {"text": line_content},
},
}
}
],
}
results_list.append(result_obj)

sarif_data["runs"][0]["tool"]["driver"]["rules"] = list(rules_map.values())
sarif_data["runs"][0]["results"] = results_list

return sarif_data

@staticmethod
def create_security_comment_json(diff: Diff) -> dict:
scan_failed = False
Expand Down
39 changes: 37 additions & 2 deletions socketsecurity/socketcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@
type=float
)

parser.add_argument(
'--enable-sarif',
help='Enable SARIF output of results instead of table or JSON format',
action='store_true',
default=False
)


def output_console_comments(diff_report: Diff, sbom_file_name: str = None) -> None:
if diff_report.id != "NO_DIFF_RAN":
console_security_comment = Messages.create_console_security_alert_table(diff_report)
Expand All @@ -188,6 +196,25 @@ def output_console_comments(diff_report: Diff, sbom_file_name: str = None) -> No
else:
log.info("No New Security issues detected by Socket Security")

def output_console_sarif(diff_report: Diff, sbom_file_name: str = None) -> None:
"""
Generate SARIF output from the diff report and save it to a file.
"""
if diff_report.id != "NO_DIFF_RAN":
# Generate the SARIF structure using Messages
console_security_comment = Messages.create_security_comment_sarif(diff_report)

# Save the SARIF output to the specified SBOM file name or fallback to a default
save_sbom_file(diff_report, sbom_file_name)
# Print the SARIF output to the console in JSON format
print(json.dumps(console_security_comment, indent=2))

# Handle exit codes based on alert severity
if not report_pass(diff_report) and not blocking_disabled:
sys.exit(1)
elif len(diff_report.new_alerts) > 0 and not blocking_disabled:
# Warning alerts without blocking
sys.exit(5)

def output_console_json(diff_report: Diff, sbom_file_name: str = None) -> None:
if diff_report.id != "NO_DIFF_RAN":
Expand Down Expand Up @@ -257,6 +284,7 @@ def main_code():
sbom_file = arguments.sbom_file
license_mode = arguments.generate_license
enable_json = arguments.enable_json
enable_sarif = arguments.enable_sarif
disable_overview = arguments.disable_overview
disable_security_issue = arguments.disable_security_issue
ignore_commit_files = arguments.ignore_commit_files
Expand Down Expand Up @@ -401,7 +429,10 @@ def main_code():
else:
log.info("Starting non-PR/MR flow")
diff = core.create_new_diff(target_path, params, workspace=target_path, no_change=no_change)
if enable_json:
if enable_sarif:
log.debug("Outputting SARIF Results")
output_console_sarif(diff, sbom_file)
elif enable_json:
log.debug("Outputting JSON Results")
output_console_json(diff, sbom_file)
else:
Expand All @@ -410,7 +441,11 @@ def main_code():
log.info("API Mode")
diff: Diff
diff = core.create_new_diff(target_path, params, workspace=target_path, no_change=no_change)
if enable_json:
if enable_sarif:
log.debug("Outputting SARIF Results")
output_console_sarif(diff, sbom_file)
elif enable_json:
log.debug("Outputting JSON Results")
output_console_json(diff, sbom_file)
else:
output_console_comments(diff, sbom_file)
Expand Down
Loading