11
11
12
12
13
13
class Messages :
14
-
14
+
15
15
@staticmethod
16
16
def map_severity_to_sarif (severity : str ) -> str :
17
17
"""
18
- Map Socket Security severity levels to SARIF levels.
18
+ Map Socket severity levels to SARIF levels (GitHub code scanning).
19
+
20
+ 'low' -> 'note'
21
+ 'medium' or 'middle' -> 'warning'
22
+ 'high' or 'critical' -> 'error'
19
23
"""
20
24
severity_mapping = {
21
25
"low" : "note" ,
22
26
"medium" : "warning" ,
23
- "middle" : "warning" ,
27
+ "middle" : "warning" , # older data might say "middle"
24
28
"high" : "error" ,
25
29
"critical" : "error" ,
26
30
}
@@ -29,81 +33,82 @@ def map_severity_to_sarif(severity: str) -> str:
29
33
@staticmethod
30
34
def find_line_in_file (packagename : str , packageversion : str , manifest_file : str ) -> tuple :
31
35
"""
32
- Given a manifest file, find the line number and snippet where the package is declared.
33
- For JSON-based manifests (e.g. package-lock.json, package.json, Pipfile.lock, composer.lock),
34
- we first verify the package exists (via JSON parsing) and then scan the raw text using one
35
- or more needle patterns.
36
- For text-based manifests, we use regex search.
36
+ Finds the line number and snippet of code for the given package/version in a manifest file.
37
+ Returns a 2-tuple: (line_number, snippet_or_message).
38
+
39
+ Supports:
40
+ 1) JSON-based manifest files (package-lock.json, Pipfile.lock, composer.lock)
41
+ - Locates a dictionary entry with the matching package & version
42
+ - Does a rough line-based search to find the actual line in the raw text
43
+ 2) Text-based (requirements.txt, package.json, yarn.lock, etc.)
44
+ - Uses compiled regex patterns to detect a match line by line
37
45
"""
46
+ # Extract just the file name to detect manifest type
38
47
file_type = Path (manifest_file ).name
39
48
40
- # --------------------
41
- # 1) JSON-based manifests
42
- # --------------------
43
- if file_type in ["package-lock.json" , "Pipfile.lock" , "composer.lock" , "package.json" ]:
49
+ # ----------------------------------------------------
50
+ # 1) JSON-based manifest files
51
+ # ----------------------------------------------------
52
+ if file_type in ["package-lock.json" , "Pipfile.lock" , "composer.lock" ]:
44
53
try :
54
+ # Read entire file so we can parse JSON and also do raw line checks
45
55
with open (manifest_file , "r" , encoding = "utf-8" ) as f :
46
56
raw_text = f .read ()
47
- try :
48
- data = json .loads (raw_text )
49
- except json .JSONDecodeError :
50
- data = {}
51
-
52
- found = False
53
- # For package.json, check both dependencies and devDependencies.
54
- if file_type == "package.json" :
55
- deps = data .get ("dependencies" , {})
56
- deps_dev = data .get ("devDependencies" , {})
57
- all_deps = {** deps , ** deps_dev }
58
- if packagename in all_deps :
59
- # Allow for versions with caret/tilde prefixes.
60
- actual_version = all_deps [packagename ]
61
- if actual_version == packageversion or actual_version .lstrip ("^~" ) == packageversion :
62
- found = True
63
- else :
64
- # For package-lock.json and similar, look into common keys.
65
- for key in ["packages" , "default" , "dependencies" ]:
66
- if key in data :
67
- packages_dict = data [key ]
68
- # Keys in package-lock.json can be "node_modules/<pkg>"
69
- for key_item , info in packages_dict .items ():
70
- if key_item .endswith (packagename ):
71
- ver = info if isinstance (info , str ) else info .get ("version" , "" )
72
- if ver == packageversion or ver .lstrip ("^~" ) == packageversion :
73
- found = True
74
- break
75
- if found :
76
- break
77
57
78
- if not found :
79
- return 1 , f'"{ packagename } ": not found in { manifest_file } '
58
+ # Attempt JSON parse
59
+ data = json .loads (raw_text )
60
+
61
+ # In practice, you may need to check data["dependencies"], data["default"], etc.
62
+ # This is an example approach.
63
+ packages_dict = (
64
+ data .get ("packages" )
65
+ or data .get ("default" )
66
+ or data .get ("dependencies" )
67
+ or {}
68
+ )
69
+
70
+ found_key = None
71
+ found_info = None
72
+ # Locate a dictionary entry whose 'version' matches
73
+ for key , value in packages_dict .items ():
74
+ # For NPM package-lock, keys might look like "node_modules/axios"
75
+ if key .endswith (packagename ) and "version" in value :
76
+ if value ["version" ] == packageversion :
77
+ found_key = key
78
+ found_info = value
79
+ break
80
80
81
- # Build one or more needle patterns. For package-lock.json, try both patterns.
82
- needles = []
83
- if file_type == "package-lock.json" :
84
- # Try with "node_modules/..." first, then without.
85
- needles .append (f'"node_modules/{ packagename } "' )
86
- needles .append (f'"{ packagename } "' )
81
+ if found_key and found_info :
82
+ # Search lines to approximate the correct line number
83
+ needle_key = f'"{ found_key } ":' # e.g. "node_modules/axios":
84
+ needle_version = f'"version": "{ packageversion } "'
85
+ lines = raw_text .splitlines ()
86
+ best_line = 1
87
+ snippet = None
88
+
89
+ for i , line in enumerate (lines , start = 1 ):
90
+ if (needle_key in line ) or (needle_version in line ):
91
+ best_line = i
92
+ snippet = line .strip ()
93
+ break # On first match, stop
94
+
95
+ # If we found an approximate line, return it; else fallback to line 1
96
+ if best_line > 0 and snippet :
97
+ return best_line , snippet
98
+ else :
99
+ return 1 , f'"{ found_key } ": { found_info } '
87
100
else :
88
- needles .append (f'"{ packagename } "' )
89
-
90
- # Scan through the file's lines to locate a matching needle.
91
- lines = raw_text .splitlines ()
92
- for i , line in enumerate (lines , start = 1 ):
93
- for needle in needles :
94
- if needle in line :
95
- return i , line .strip ()
96
- return 1 , f'"{ packagename } ": declaration not found'
97
- except FileNotFoundError :
98
- return 1 , f"{ manifest_file } not found"
99
- except Exception as e :
100
- return 1 , f"Error reading { manifest_file } : { e } "
101
-
102
- # --------------------
101
+ return 1 , f"{ packagename } { packageversion } (not found in { manifest_file } )"
102
+
103
+ except (FileNotFoundError , json .JSONDecodeError ):
104
+ return 1 , f"Error reading { manifest_file } "
105
+
106
+ # ----------------------------------------------------
103
107
# 2) Text-based / line-based manifests
104
- # --------------------
105
- # Define regex patterns for common text-based manifest types.
108
+ # ----------------------------------------------------
109
+ # Define a dictionary of patterns for common manifest types
106
110
search_patterns = {
111
+ "package.json" : rf'"{ packagename } ":\s*"{ packageversion } "' ,
107
112
"yarn.lock" : rf'{ packagename } @{ packageversion } ' ,
108
113
"pnpm-lock.yaml" : rf'"{ re .escape (packagename )} "\s*:\s*\{{[^}}]*"version":\s*"{ re .escape (packageversion )} "' ,
109
114
"requirements.txt" : rf'^{ re .escape (packagename )} \s*(?:==|===|!=|>=|<=|~=|\s+)?\s*{ re .escape (packageversion )} (?:\s*;.*)?$' ,
@@ -127,24 +132,30 @@ def find_line_in_file(packagename: str, packageversion: str, manifest_file: str)
127
132
"conanfile.txt" : rf'{ re .escape (packagename )} /{ re .escape (packageversion )} ' ,
128
133
"vcpkg.json" : rf'"{ re .escape (packagename )} ":\s*"{ re .escape (packageversion )} "' ,
129
134
}
135
+
136
+ # If no specific pattern is found for this file name, fallback to a naive approach
130
137
searchstring = search_patterns .get (file_type , rf'{ re .escape (packagename )} .*{ re .escape (packageversion )} ' )
131
138
try :
139
+ # Read file lines and search for a match
132
140
with open (manifest_file , 'r' , encoding = "utf-8" ) as file :
133
141
lines = [line .rstrip ("\n " ) for line in file ]
134
142
for line_number , line_content in enumerate (lines , start = 1 ):
143
+ # For Python conditional dependencies, ignore everything after first ';'
135
144
line_main = line_content .split (";" , 1 )[0 ].strip ()
145
+ # Use a case-insensitive regex search
136
146
if re .search (searchstring , line_main , re .IGNORECASE ):
137
147
return line_number , line_content .strip ()
138
148
except FileNotFoundError :
139
149
return 1 , f"{ manifest_file } not found"
140
150
except Exception as e :
141
151
return 1 , f"Error reading { manifest_file } : { e } "
152
+
142
153
return 1 , f"{ packagename } { packageversion } (not found)"
143
154
144
155
@staticmethod
145
156
def get_manifest_type_url (manifest_file : str , pkg_name : str , pkg_version : str ) -> str :
146
157
"""
147
- Determine the URL prefix based on the manifest file.
158
+ Determine the correct URL path based on the manifest file type .
148
159
"""
149
160
manifest_to_url_prefix = {
150
161
"package.json" : "npm" ,
@@ -167,6 +178,7 @@ def get_manifest_type_url(manifest_file: str, pkg_name: str, pkg_version: str) -
167
178
"composer.json" : "composer" ,
168
179
"vcpkg.json" : "vcpkg" ,
169
180
}
181
+
170
182
file_type = Path (manifest_file ).name
171
183
url_prefix = manifest_to_url_prefix .get (file_type , "unknown" )
172
184
return f"https://socket.dev/{ url_prefix } /package/{ pkg_name } /alerts/{ pkg_version } "
@@ -176,7 +188,7 @@ def create_security_comment_sarif(diff) -> dict:
176
188
"""
177
189
Create SARIF-compliant output from the diff report, including dynamic URL generation
178
190
based on manifest type and improved <br/> formatting for GitHub SARIF display.
179
-
191
+
180
192
This function now:
181
193
- Accepts multiple manifest files from alert.introduced_by or alert.manifests.
182
194
- Generates one SARIF location per manifest file.
@@ -215,6 +227,7 @@ def create_security_comment_sarif(diff) -> dict:
215
227
severity = alert .severity
216
228
217
229
# --- Determine manifest files from alert data ---
230
+ # Instead of using a single manifest file, split the values.
218
231
manifest_files = []
219
232
if alert .introduced_by and isinstance (alert .introduced_by , list ):
220
233
for entry in alert .introduced_by :
@@ -232,10 +245,10 @@ def create_security_comment_sarif(diff) -> dict:
232
245
# Use the first manifest for URL generation.
233
246
socket_url = Messages .get_manifest_type_url (manifest_files [0 ], pkg_name , pkg_version )
234
247
235
- # Prepare the short and full descriptions .
248
+ # Prepare descriptions with <br/> replacements .
236
249
short_desc = (
237
- f"{ alert .props .get ('note' , '' )} <br/><br/>Suggested Action:<br/>"
238
- f"{ alert . suggestion } <br/><a href=\" { socket_url } \" >{ socket_url } </a>"
250
+ f"{ alert .props .get ('note' , '' )} <br/><br/>Suggested Action:<br/>{ alert . suggestion } "
251
+ f"<br/><a href=\" { socket_url } \" >{ socket_url } </a>"
239
252
)
240
253
full_desc = "{} - {}" .format (alert .title , alert .description .replace ('\r \n ' , '<br/>' ))
241
254
@@ -268,15 +281,15 @@ def create_security_comment_sarif(diff) -> dict:
268
281
}
269
282
})
270
283
271
- # Create the SARIF result for this alert.
284
+ # Create the SARIF result for this alert with multiple locations .
272
285
result_obj = {
273
286
"ruleId" : rule_id ,
274
287
"message" : {"text" : short_desc },
275
288
"locations" : locations ,
276
289
}
277
290
results_list .append (result_obj )
278
291
279
- # Attach the collected rules and results.
292
+ # Attach rules and results.
280
293
sarif_data ["runs" ][0 ]["tool" ]["driver" ]["rules" ] = list (rules_map .values ())
281
294
sarif_data ["runs" ][0 ]["results" ] = results_list
282
295
0 commit comments