diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index eaabf14..28295e5 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -1,4 +1,5 @@ import json +import os from mdutils import MdUtils from socketsecurity.core.classes import Diff, Purl, Issue @@ -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 diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index a0da7d8..f2bc0cb 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -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) @@ -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": @@ -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 @@ -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: @@ -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)