Skip to content

Commit 7ddb453

Browse files
obarreraOrlando Barrera II
andauthored
Improved file line number glob. (#46)
* Improved file line number glob. Added link to socket.dev for package alert. * Updated version --------- Co-authored-by: Orlando Barrera II <[email protected]>
1 parent 0b8b2fe commit 7ddb453

File tree

2 files changed

+153
-30
lines changed

2 files changed

+153
-30
lines changed

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
__author__ = 'socket.dev'
2-
__version__ = '1.0.43'
2+
__version__ = '1.0.44'

socketsecurity/core/messages.py

Lines changed: 152 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import json
22
import os
3+
import re
4+
import json
35

6+
from pathlib import Path
47
from mdutils import MdUtils
58
from socketsecurity.core.classes import Diff, Purl, Issue
69
from prettytable import PrettyTable
@@ -12,6 +15,10 @@ class Messages:
1215
def map_severity_to_sarif(severity: str) -> str:
1316
"""
1417
Map Socket severity levels to SARIF levels (GitHub code scanning).
18+
19+
'low' -> 'note'
20+
'medium' or 'middle' -> 'warning'
21+
'high' or 'critical' -> 'error'
1522
"""
1623
severity_mapping = {
1724
"low": "note",
@@ -22,39 +29,147 @@ def map_severity_to_sarif(severity: str) -> str:
2229
}
2330
return severity_mapping.get(severity.lower(), "note")
2431

25-
2632
@staticmethod
27-
def find_line_in_file(pkg_name: str, manifest_file: str) -> tuple[int, str]:
33+
def find_line_in_file(packagename: str, packageversion: str, manifest_file: str) -> tuple:
2834
"""
29-
Search 'manifest_file' for 'pkg_name'.
30-
Return (line_number, line_content) if found, else (1, fallback).
35+
Finds the line number and snippet of code for the given package/version in a manifest file.
36+
Returns a 2-tuple: (line_number, snippet_or_message).
37+
38+
Supports:
39+
1) JSON-based manifest files (package-lock.json, Pipfile.lock, composer.lock)
40+
- Locates a dictionary entry with the matching package & version
41+
- Does a rough line-based search to find the actual line in the raw text
42+
2) Text-based (requirements.txt, package.json, yarn.lock, etc.)
43+
- Uses compiled regex patterns to detect a match line by line
3144
"""
32-
if not manifest_file or not os.path.isfile(manifest_file):
33-
return 1, f"[No {manifest_file or 'manifest'} found in repo]"
45+
# Extract just the file name to detect manifest type
46+
file_type = Path(manifest_file).name
47+
48+
# ----------------------------------------------------
49+
# 1) JSON-based manifest files
50+
# ----------------------------------------------------
51+
if file_type in ["package-lock.json", "Pipfile.lock", "composer.lock"]:
52+
try:
53+
# Read entire file so we can parse JSON and also do raw line checks
54+
with open(manifest_file, "r", encoding="utf-8") as f:
55+
raw_text = f.read()
56+
57+
# Attempt JSON parse
58+
data = json.loads(raw_text)
59+
60+
# In practice, you may need to check data["dependencies"], data["default"], etc.
61+
# This is an example approach.
62+
packages_dict = (
63+
data.get("packages")
64+
or data.get("default")
65+
or data.get("dependencies")
66+
or {}
67+
)
68+
69+
found_key = None
70+
found_info = None
71+
# Locate a dictionary entry whose 'version' matches
72+
for key, value in packages_dict.items():
73+
# For NPM package-lock, keys might look like "node_modules/axios"
74+
if key.endswith(packagename) and "version" in value:
75+
if value["version"] == packageversion:
76+
found_key = key
77+
found_info = value
78+
break
79+
80+
if found_key and found_info:
81+
# Search lines to approximate the correct line number
82+
needle_key = f'"{found_key}":' # e.g. "node_modules/axios":
83+
needle_version = f'"version": "{packageversion}"'
84+
lines = raw_text.splitlines()
85+
best_line = -1
86+
snippet = None
87+
88+
for i, line in enumerate(lines, start=1):
89+
if (needle_key in line) or (needle_version in line):
90+
best_line = i
91+
snippet = line.strip()
92+
break # On first match, stop
93+
94+
# If we found an approximate line, return it; else fallback to line 1
95+
if best_line > 0 and snippet:
96+
return best_line, snippet
97+
else:
98+
return 1, f'"{found_key}": {found_info}'
99+
else:
100+
return -1, f"{packagename} {packageversion} (not found in {manifest_file})"
101+
102+
except (FileNotFoundError, json.JSONDecodeError):
103+
return -1, f"Error reading {manifest_file}"
104+
105+
# ----------------------------------------------------
106+
# 2) Text-based / line-based manifests
107+
# ----------------------------------------------------
108+
# Define a dictionary of patterns for common manifest types
109+
search_patterns = {
110+
"package.json": rf'"{packagename}":\s*"{packageversion}"',
111+
"yarn.lock": rf'{packagename}@{packageversion}',
112+
"pnpm-lock.yaml": rf'"{re.escape(packagename)}"\s*:\s*\{{[^}}]*"version":\s*"{re.escape(packageversion)}"',
113+
"requirements.txt": rf'^{re.escape(packagename)}\s*(?:==|===|!=|>=|<=|~=|\s+)?\s*{re.escape(packageversion)}(?:\s*;.*)?$',
114+
"pyproject.toml": rf'{packagename}\s*=\s*"{packageversion}"',
115+
"Pipfile": rf'"{packagename}"\s*=\s*"{packageversion}"',
116+
"go.mod": rf'require\s+{re.escape(packagename)}\s+{re.escape(packageversion)}',
117+
"go.sum": rf'{re.escape(packagename)}\s+{re.escape(packageversion)}',
118+
"pom.xml": rf'<artifactId>{re.escape(packagename)}</artifactId>\s*<version>{re.escape(packageversion)}</version>',
119+
"build.gradle": rf'implementation\s+"{re.escape(packagename)}:{re.escape(packageversion)}"',
120+
"Gemfile": rf'gem\s+"{re.escape(packagename)}",\s*"{re.escape(packageversion)}"',
121+
"Gemfile.lock": rf'\s+{re.escape(packagename)}\s+\({re.escape(packageversion)}\)',
122+
".csproj": rf'<PackageReference\s+Include="{re.escape(packagename)}"\s+Version="{re.escape(packageversion)}"\s*/>',
123+
".fsproj": rf'<PackageReference\s+Include="{re.escape(packagename)}"\s+Version="{re.escape(packageversion)}"\s*/>',
124+
"paket.dependencies": rf'nuget\s+{re.escape(packagename)}\s+{re.escape(packageversion)}',
125+
"Cargo.toml": rf'{re.escape(packagename)}\s*=\s*"{re.escape(packageversion)}"',
126+
"build.sbt": rf'"{re.escape(packagename)}"\s*%\s*"{re.escape(packageversion)}"',
127+
"Podfile": rf'pod\s+"{re.escape(packagename)}",\s*"{re.escape(packageversion)}"',
128+
"Package.swift": rf'\.package\(name:\s*"{re.escape(packagename)}",\s*url:\s*".*?",\s*version:\s*"{re.escape(packageversion)}"\)',
129+
"mix.exs": rf'\{{:{re.escape(packagename)},\s*"{re.escape(packageversion)}"\}}',
130+
"composer.json": rf'"{re.escape(packagename)}":\s*"{re.escape(packageversion)}"',
131+
"conanfile.txt": rf'{re.escape(packagename)}/{re.escape(packageversion)}',
132+
"vcpkg.json": rf'"{re.escape(packagename)}":\s*"{re.escape(packageversion)}"',
133+
}
134+
135+
# If no specific pattern is found for this file name, fallback to a naive approach
136+
searchstring = search_patterns.get(file_type, rf'{re.escape(packagename)}.*{re.escape(packageversion)}')
137+
34138
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")
139+
# Read file lines and search for a match
140+
with open(manifest_file, 'r', encoding="utf-8") as file:
141+
lines = [line.rstrip("\n") for line in file]
142+
for line_number, line_content in enumerate(lines, start=1):
143+
# For Python conditional dependencies, ignore everything after first ';'
144+
line_main = line_content.split(";", 1)[0].strip()
145+
146+
# Use a case-insensitive regex search
147+
if re.search(searchstring, line_main, re.IGNORECASE):
148+
return line_number, line_content.strip()
149+
150+
except FileNotFoundError:
151+
return -1, f"{manifest_file} not found"
40152
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-
153+
return -1, f"Error reading {manifest_file}: {e}"
154+
155+
return -1, f"{packagename} {packageversion} (not found)"
156+
44157
@staticmethod
45158
def create_security_comment_sarif(diff: Diff) -> dict:
46159
"""
47-
Create SARIF-compliant output from the diff report.
160+
Create SARIF-compliant output from the diff report, including line references
161+
and a link to the Socket docs in the fullDescription. Also converts any \r\n
162+
into <br/> so they render properly in GitHub's SARIF display.
48163
"""
164+
# Check if there's a blocking error in new alerts
49165
scan_failed = False
50166
if len(diff.new_alerts) == 0:
51167
for alert in diff.new_alerts:
52-
alert: Issue
53168
if alert.error:
54169
scan_failed = True
55170
break
56171

57-
# Basic SARIF structure
172+
# Basic SARIF skeleton
58173
sarif_data = {
59174
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
60175
"version": "2.1.0",
@@ -76,38 +191,45 @@ def create_security_comment_sarif(diff: Diff) -> dict:
76191
results_list = []
77192

78193
for alert in diff.new_alerts:
79-
alert: Issue
80194
pkg_name = alert.pkg_name
81195
pkg_version = alert.pkg_version
82196
rule_id = f"{pkg_name}=={pkg_version}"
83197
severity = alert.severity
84198

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}"
199+
# Convert any \r\n in short desc to <br/> so they display properly
200+
short_desc_raw = f"{alert.props.get('note', '')}\r\n\r\nSuggested Action:\r\n{alert.suggestion}"
201+
short_desc = short_desc_raw.replace("\r\n", "<br/>")
89202

90-
# Find the manifest file and line details
203+
# Build link to Socket docs, e.g. "https://socket.dev/npm/package/foo/alerts/1.2.3"
204+
socket_url = f"https://socket.dev/npm/package/{pkg_name}/alerts/{pkg_version}"
205+
206+
# Also convert \r\n in the main description to <br/>, then append the Socket docs link
207+
base_desc = alert.description.replace("\r\n", "<br/>")
208+
full_desc_raw = f"{alert.title} - {base_desc}<br/>{socket_url}"
209+
210+
# Identify the manifest file and line
91211
introduced_list = alert.introduced_by
92212
if introduced_list and isinstance(introduced_list[0], list) and len(introduced_list[0]) > 1:
93213
manifest_file = introduced_list[0][1]
94214
else:
95215
manifest_file = alert.manifests or "requirements.txt"
96216

97-
line_number, line_content = Messages.find_line_in_file(pkg_name, manifest_file)
217+
line_number, line_content = Messages.find_line_in_file(pkg_name, pkg_version, manifest_file)
98218

99-
# Define the rule if not already defined
219+
# If not already defined, create a rule for this package
100220
if rule_id not in rules_map:
101221
rules_map[rule_id] = {
102222
"id": rule_id,
103223
"name": f"{pkg_name}=={pkg_version}",
104-
"shortDescription": {"text": title},
105-
"fullDescription": {"text": full_desc},
224+
"shortDescription": {"text": f"Alert generated for {rule_id} by Socket Security"},
225+
"fullDescription": {"text": full_desc_raw},
106226
"helpUri": alert.url,
107-
"defaultConfiguration": {"level": Messages.map_severity_to_sarif(severity)},
227+
"defaultConfiguration": {
228+
"level": Messages.map_severity_to_sarif(severity)
229+
},
108230
}
109231

110-
# Add the result
232+
# Create a SARIF "result" referencing the line where we found the match
111233
result_obj = {
112234
"ruleId": rule_id,
113235
"message": {"text": short_desc},
@@ -125,6 +247,7 @@ def create_security_comment_sarif(diff: Diff) -> dict:
125247
}
126248
results_list.append(result_obj)
127249

250+
# Attach our rules and results to the SARIF data
128251
sarif_data["runs"][0]["tool"]["driver"]["rules"] = list(rules_map.values())
129252
sarif_data["runs"][0]["results"] = results_list
130253

0 commit comments

Comments
 (0)