From 8b63fc22bbdc1b447bbf1811dcd1b5065e0aa910 Mon Sep 17 00:00:00 2001 From: Orlando Barrera II Date: Fri, 31 Jan 2025 13:26:25 -0600 Subject: [PATCH 1/4] Improved file line number glob. Added link to socket.dev for package alert. --- socketsecurity/core/messages.py | 181 +++++++++++++++++++++++++++----- 1 file changed, 152 insertions(+), 29 deletions(-) diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index 28295e5..f914bb9 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -1,6 +1,9 @@ import json import os +import re +import json +from pathlib import Path from mdutils import MdUtils from socketsecurity.core.classes import Diff, Purl, Issue from prettytable import PrettyTable @@ -12,6 +15,10 @@ class Messages: def map_severity_to_sarif(severity: str) -> str: """ Map Socket severity levels to SARIF levels (GitHub code scanning). + + 'low' -> 'note' + 'medium' or 'middle' -> 'warning' + 'high' or 'critical' -> 'error' """ severity_mapping = { "low": "note", @@ -22,39 +29,147 @@ def map_severity_to_sarif(severity: str) -> str: } return severity_mapping.get(severity.lower(), "note") - @staticmethod - def find_line_in_file(pkg_name: str, manifest_file: str) -> tuple[int, str]: + def find_line_in_file(packagename: str, packageversion: str, manifest_file: str) -> tuple: """ - Search 'manifest_file' for 'pkg_name'. - Return (line_number, line_content) if found, else (1, fallback). + Finds the line number and snippet of code for the given package/version in a manifest file. + Returns a 2-tuple: (line_number, snippet_or_message). + + Supports: + 1) JSON-based manifest files (package-lock.json, Pipfile.lock, composer.lock) + - Locates a dictionary entry with the matching package & version + - Does a rough line-based search to find the actual line in the raw text + 2) Text-based (requirements.txt, package.json, yarn.lock, etc.) + - Uses compiled regex patterns to detect a match line by line """ - if not manifest_file or not os.path.isfile(manifest_file): - return 1, f"[No {manifest_file or 'manifest'} found in repo]" + # Extract just the file name to detect manifest type + file_type = Path(manifest_file).name + + # ---------------------------------------------------- + # 1) JSON-based manifest files + # ---------------------------------------------------- + if file_type in ["package-lock.json", "Pipfile.lock", "composer.lock"]: + try: + # Read entire file so we can parse JSON and also do raw line checks + with open(manifest_file, "r", encoding="utf-8") as f: + raw_text = f.read() + + # Attempt JSON parse + data = json.loads(raw_text) + + # In practice, you may need to check data["dependencies"], data["default"], etc. + # This is an example approach. + packages_dict = ( + data.get("packages") + or data.get("default") + or data.get("dependencies") + or {} + ) + + found_key = None + found_info = None + # Locate a dictionary entry whose 'version' matches + for key, value in packages_dict.items(): + # For NPM package-lock, keys might look like "node_modules/axios" + if key.endswith(packagename) and "version" in value: + if value["version"] == packageversion: + found_key = key + found_info = value + break + + if found_key and found_info: + # Search lines to approximate the correct line number + needle_key = f'"{found_key}":' # e.g. "node_modules/axios": + needle_version = f'"version": "{packageversion}"' + lines = raw_text.splitlines() + best_line = -1 + snippet = None + + for i, line in enumerate(lines, start=1): + if (needle_key in line) or (needle_version in line): + best_line = i + snippet = line.strip() + break # On first match, stop + + # If we found an approximate line, return it; else fallback to line 1 + if best_line > 0 and snippet: + return best_line, snippet + else: + return 1, f'"{found_key}": {found_info}' + else: + return -1, f"{packagename} {packageversion} (not found in {manifest_file})" + + except (FileNotFoundError, json.JSONDecodeError): + return -1, f"Error reading {manifest_file}" + + # ---------------------------------------------------- + # 2) Text-based / line-based manifests + # ---------------------------------------------------- + # Define a dictionary of patterns for common manifest types + search_patterns = { + "package.json": rf'"{packagename}":\s*"{packageversion}"', + "yarn.lock": rf'{packagename}@{packageversion}', + "pnpm-lock.yaml": rf'"{re.escape(packagename)}"\s*:\s*\{{[^}}]*"version":\s*"{re.escape(packageversion)}"', + "requirements.txt": rf'^{re.escape(packagename)}\s*(?:==|===|!=|>=|<=|~=|\s+)?\s*{re.escape(packageversion)}(?:\s*;.*)?$', + "pyproject.toml": rf'{packagename}\s*=\s*"{packageversion}"', + "Pipfile": rf'"{packagename}"\s*=\s*"{packageversion}"', + "go.mod": rf'require\s+{re.escape(packagename)}\s+{re.escape(packageversion)}', + "go.sum": rf'{re.escape(packagename)}\s+{re.escape(packageversion)}', + "pom.xml": rf'{re.escape(packagename)}\s*{re.escape(packageversion)}', + "build.gradle": rf'implementation\s+"{re.escape(packagename)}:{re.escape(packageversion)}"', + "Gemfile": rf'gem\s+"{re.escape(packagename)}",\s*"{re.escape(packageversion)}"', + "Gemfile.lock": rf'\s+{re.escape(packagename)}\s+\({re.escape(packageversion)}\)', + ".csproj": rf'', + ".fsproj": rf'', + "paket.dependencies": rf'nuget\s+{re.escape(packagename)}\s+{re.escape(packageversion)}', + "Cargo.toml": rf'{re.escape(packagename)}\s*=\s*"{re.escape(packageversion)}"', + "build.sbt": rf'"{re.escape(packagename)}"\s*%\s*"{re.escape(packageversion)}"', + "Podfile": rf'pod\s+"{re.escape(packagename)}",\s*"{re.escape(packageversion)}"', + "Package.swift": rf'\.package\(name:\s*"{re.escape(packagename)}",\s*url:\s*".*?",\s*version:\s*"{re.escape(packageversion)}"\)', + "mix.exs": rf'\{{:{re.escape(packagename)},\s*"{re.escape(packageversion)}"\}}', + "composer.json": rf'"{re.escape(packagename)}":\s*"{re.escape(packageversion)}"', + "conanfile.txt": rf'{re.escape(packagename)}/{re.escape(packageversion)}', + "vcpkg.json": rf'"{re.escape(packagename)}":\s*"{re.escape(packageversion)}"', + } + + # If no specific pattern is found for this file name, fallback to a naive approach + searchstring = search_patterns.get(file_type, rf'{re.escape(packagename)}.*{re.escape(packageversion)}') + 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") + # Read file lines and search for a match + with open(manifest_file, 'r', encoding="utf-8") as file: + lines = [line.rstrip("\n") for line in file] + for line_number, line_content in enumerate(lines, start=1): + # For Python conditional dependencies, ignore everything after first ';' + line_main = line_content.split(";", 1)[0].strip() + + # Use a case-insensitive regex search + if re.search(searchstring, line_main, re.IGNORECASE): + return line_number, line_content.strip() + + except FileNotFoundError: + return -1, f"{manifest_file} not found" except Exception as e: - return 1, f"[Error reading {manifest_file}: {e}]" - return 1, f"[Package '{pkg_name}' not found in {manifest_file}]" - + return -1, f"Error reading {manifest_file}: {e}" + + return -1, f"{packagename} {packageversion} (not found)" + @staticmethod def create_security_comment_sarif(diff: Diff) -> dict: """ - Create SARIF-compliant output from the diff report. + Create SARIF-compliant output from the diff report, including line references + and a link to the Socket docs in the fullDescription. Also converts any \r\n + into
so they render properly in GitHub's SARIF display. """ + # Check if there's a blocking error in new alerts 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 + # Basic SARIF skeleton sarif_data = { "$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", @@ -76,38 +191,45 @@ def create_security_comment_sarif(diff: Diff) -> dict: 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}" + # Convert any \r\n in short desc to
so they display properly + short_desc_raw = f"{alert.props.get('note', '')}\r\n\r\nSuggested Action:\r\n{alert.suggestion}" + short_desc = short_desc_raw.replace("\r\n", "
") - # Find the manifest file and line details + # Build link to Socket docs, e.g. "https://socket.dev/npm/package/foo/alerts/1.2.3" + socket_url = f"https://socket.dev/npm/package/{pkg_name}/alerts/{pkg_version}" + + # Also convert \r\n in the main description to
, then append the Socket docs link + base_desc = alert.description.replace("\r\n", "
") + full_desc_raw = f"{alert.title} - {base_desc}
{socket_url}" + + # Identify the manifest file and line 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) + line_number, line_content = Messages.find_line_in_file(pkg_name, pkg_version, manifest_file) - # Define the rule if not already defined + # If not already defined, create a rule for this package 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}, + "shortDescription": {"text": f"Alert generated for {rule_id} by Socket Security"}, + "fullDescription": {"text": full_desc_raw}, "helpUri": alert.url, - "defaultConfiguration": {"level": Messages.map_severity_to_sarif(severity)}, + "defaultConfiguration": { + "level": Messages.map_severity_to_sarif(severity) + }, } - # Add the result + # Create a SARIF "result" referencing the line where we found the match result_obj = { "ruleId": rule_id, "message": {"text": short_desc}, @@ -125,6 +247,7 @@ def create_security_comment_sarif(diff: Diff) -> dict: } results_list.append(result_obj) + # Attach our rules and results to the SARIF data sarif_data["runs"][0]["tool"]["driver"]["rules"] = list(rules_map.values()) sarif_data["runs"][0]["results"] = results_list From ed6696e4e06621e4298a5a0e4ea1d101dab7ad3b Mon Sep 17 00:00:00 2001 From: Orlando Barrera II Date: Fri, 31 Jan 2025 13:34:05 -0600 Subject: [PATCH 2/4] Updated version --- socketsecurity/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 023baaa..6e9694e 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,2 +1,2 @@ __author__ = 'socket.dev' -__version__ = '1.0.43' +__version__ = '1.0.44' From f08be0ede8cd969d389337c3b427556e2b027a57 Mon Sep 17 00:00:00 2001 From: Orlando Barrera II Date: Fri, 31 Jan 2025 13:52:19 -0600 Subject: [PATCH 3/4] Removed -1 ftom sarif --- socketsecurity/__init__.py | 2 +- socketsecurity/core/messages.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 6e9694e..0cfc592 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,2 +1,2 @@ __author__ = 'socket.dev' -__version__ = '1.0.44' +__version__ = '1.0.45' diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index f914bb9..bb09395 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -82,7 +82,7 @@ def find_line_in_file(packagename: str, packageversion: str, manifest_file: str) needle_key = f'"{found_key}":' # e.g. "node_modules/axios": needle_version = f'"version": "{packageversion}"' lines = raw_text.splitlines() - best_line = -1 + best_line = 1 snippet = None for i, line in enumerate(lines, start=1): @@ -97,10 +97,10 @@ def find_line_in_file(packagename: str, packageversion: str, manifest_file: str) else: return 1, f'"{found_key}": {found_info}' else: - return -1, f"{packagename} {packageversion} (not found in {manifest_file})" + return 1, f"{packagename} {packageversion} (not found in {manifest_file})" except (FileNotFoundError, json.JSONDecodeError): - return -1, f"Error reading {manifest_file}" + return 1, f"Error reading {manifest_file}" # ---------------------------------------------------- # 2) Text-based / line-based manifests From 647690334cc673f4527fcadcabfea900e94eeadf Mon Sep 17 00:00:00 2001 From: Orlando Barrera II Date: Fri, 31 Jan 2025 14:17:13 -0600 Subject: [PATCH 4/4] Added the Socket Package link to the alert info --- socketsecurity/__init__.py | 2 +- socketsecurity/core/messages.py | 87 +++++++++++++++++++-------------- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 0cfc592..af87e7f 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,2 +1,2 @@ __author__ = 'socket.dev' -__version__ = '1.0.45' +__version__ = '1.0.46' diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index bb09395..eef755f 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -148,28 +148,49 @@ def find_line_in_file(packagename: str, packageversion: str, manifest_file: str) return line_number, line_content.strip() except FileNotFoundError: - return -1, f"{manifest_file} not found" + return 1, f"{manifest_file} not found" except Exception as e: - return -1, f"Error reading {manifest_file}: {e}" + return 1, f"Error reading {manifest_file}: {e}" - return -1, f"{packagename} {packageversion} (not found)" + return 1, f"{packagename} {packageversion} (not found)" @staticmethod - def create_security_comment_sarif(diff: Diff) -> dict: + def get_manifest_type_url(manifest_file: str, pkg_name: str, pkg_version: str) -> str: """ - Create SARIF-compliant output from the diff report, including line references - and a link to the Socket docs in the fullDescription. Also converts any \r\n - into
so they render properly in GitHub's SARIF display. + Determine the correct URL path based on the manifest file type. """ - # Check if there's a blocking error in new alerts - scan_failed = False - if len(diff.new_alerts) == 0: - for alert in diff.new_alerts: - if alert.error: - scan_failed = True - break + manifest_to_url_prefix = { + "package.json": "npm", + "package-lock.json": "npm", + "yarn.lock": "npm", + "pnpm-lock.yaml": "npm", + "requirements.txt": "pypi", + "pyproject.toml": "pypi", + "Pipfile": "pypi", + "go.mod": "go", + "go.sum": "go", + "pom.xml": "maven", + "build.gradle": "maven", + ".csproj": "nuget", + ".fsproj": "nuget", + "paket.dependencies": "nuget", + "Cargo.toml": "cargo", + "Gemfile": "rubygems", + "Gemfile.lock": "rubygems", + "composer.json": "composer", + "vcpkg.json": "vcpkg", + } + + file_type = Path(manifest_file).name + url_prefix = manifest_to_url_prefix.get(file_type, "unknown") + return f"https://socket.dev/{url_prefix}/package/{pkg_name}/alerts/{pkg_version}" - # Basic SARIF skeleton + @staticmethod + def create_security_comment_sarif(diff) -> dict: + """ + Create SARIF-compliant output from the diff report, including dynamic URL generation + based on manifest type and improved
formatting for GitHub SARIF display. + """ sarif_data = { "$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", @@ -196,40 +217,34 @@ def create_security_comment_sarif(diff: Diff) -> dict: rule_id = f"{pkg_name}=={pkg_version}" severity = alert.severity - # Convert any \r\n in short desc to
so they display properly - short_desc_raw = f"{alert.props.get('note', '')}\r\n\r\nSuggested Action:\r\n{alert.suggestion}" - short_desc = short_desc_raw.replace("\r\n", "
") - - # Build link to Socket docs, e.g. "https://socket.dev/npm/package/foo/alerts/1.2.3" - socket_url = f"https://socket.dev/npm/package/{pkg_name}/alerts/{pkg_version}" - - # Also convert \r\n in the main description to
, then append the Socket docs link - base_desc = alert.description.replace("\r\n", "
") - full_desc_raw = f"{alert.title} - {base_desc}
{socket_url}" - - # Identify the manifest file and line + # Generate the correct URL for the alert based on manifest type 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" + manifest_file = introduced_list[0][1] if introduced_list and isinstance(introduced_list[0], list) else alert.manifests or "requirements.txt" + socket_url = Messages.get_manifest_type_url(manifest_file, pkg_name, pkg_version) + + # Prepare descriptions with
replacements + short_desc = f"{alert.props.get('note', '')}

Suggested Action:
{alert.suggestion}" + full_desc = f"{alert.title} - {alert.description.replace('\r\n', '
')}
{socket_url}" + # Identify the line and snippet in the manifest file line_number, line_content = Messages.find_line_in_file(pkg_name, pkg_version, manifest_file) + if line_number < 1: + line_number = 1 # Ensure SARIF compliance - # If not already defined, create a rule for this package + # Create 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": f"Alert generated for {rule_id} by Socket Security"}, - "fullDescription": {"text": full_desc_raw}, - "helpUri": alert.url, + "fullDescription": {"text": full_desc}, + "helpUri": socket_url, "defaultConfiguration": { "level": Messages.map_severity_to_sarif(severity) }, } - # Create a SARIF "result" referencing the line where we found the match + # Add the SARIF result result_obj = { "ruleId": rule_id, "message": {"text": short_desc}, @@ -247,7 +262,7 @@ def create_security_comment_sarif(diff: Diff) -> dict: } results_list.append(result_obj) - # Attach our rules and results to the SARIF data + # Attach rules and results sarif_data["runs"][0]["tool"]["driver"]["rules"] = list(rules_map.values()) sarif_data["runs"][0]["results"] = results_list