@@ -15,16 +15,12 @@ class Messages:
15
15
@staticmethod
16
16
def map_severity_to_sarif (severity : str ) -> str :
17
17
"""
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'
18
+ Map Socket Security severity levels to SARIF levels.
23
19
"""
24
20
severity_mapping = {
25
21
"low" : "note" ,
26
22
"medium" : "warning" ,
27
- "middle" : "warning" , # older data might say "middle"
23
+ "middle" : "warning" ,
28
24
"high" : "error" ,
29
25
"critical" : "error" ,
30
26
}
@@ -33,82 +29,67 @@ def map_severity_to_sarif(severity: str) -> str:
33
29
@staticmethod
34
30
def find_line_in_file (packagename : str , packageversion : str , manifest_file : str ) -> tuple :
35
31
"""
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
32
+ Given a manifest file, find the line number and snippet where the package is declared.
33
+ For JSON-based manifests (package-lock.json, Pipfile.lock, composer.lock, package.json),
34
+ we attempt to parse the JSON to verify the package is present, then search for the key.
35
+ For text-based manifests, we use a regex search.
45
36
"""
46
- # Extract just the file name to detect manifest type
47
37
file_type = Path (manifest_file ).name
48
38
49
- # ----------------------------------------------------
50
- # 1) JSON-based manifest files
51
- # ----------------------------------------------------
52
- if file_type in ["package-lock.json" , "Pipfile.lock" , "composer.lock" ]:
39
+ # Handle JSON-based files.
40
+ if file_type in ["package-lock.json" , "Pipfile.lock" , "composer.lock" , "package.json" ]:
53
41
try :
54
- # Read entire file so we can parse JSON and also do raw line checks
55
42
with open (manifest_file , "r" , encoding = "utf-8" ) as f :
56
43
raw_text = f .read ()
57
-
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
-
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 } '
44
+ try :
45
+ data = json .loads (raw_text )
46
+ except json .JSONDecodeError :
47
+ data = {}
48
+
49
+ found = False
50
+ # For package.json, check dependencies and devDependencies.
51
+ if file_type == "package.json" :
52
+ deps = data .get ("dependencies" , {})
53
+ deps_dev = data .get ("devDependencies" , {})
54
+ all_deps = {** deps , ** deps_dev }
55
+ if packagename in all_deps :
56
+ actual_version = all_deps [packagename ]
57
+ # Allow for versions with caret/tilde prefixes.
58
+ if actual_version == packageversion or actual_version .lstrip ("^~" ) == packageversion :
59
+ found = True
100
60
else :
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 } "
61
+ # For other JSON-based manifests, look into common keys.
62
+ for key in ["packages" , "default" , "dependencies" ]:
63
+ if key in data :
64
+ packages_dict = data [key ]
65
+ # In package-lock.json, keys can be paths (e.g. "node_modules/axios")
66
+ for key_item , info in packages_dict .items ():
67
+ if key_item .endswith (packagename ):
68
+ # info may be a dict (with "version") or a simple version string.
69
+ ver = info if isinstance (info , str ) else info .get ("version" , "" )
70
+ if ver == packageversion :
71
+ found = True
72
+ break
73
+ if found :
74
+ break
105
75
106
- # ----------------------------------------------------
107
- # 2) Text-based / line-based manifests
108
- # ----------------------------------------------------
109
- # Define a dictionary of patterns for common manifest types
76
+ if not found :
77
+ return 1 , f'"{ packagename } ": not found in { manifest_file } '
78
+
79
+ # Now search the raw text to locate the declaration line.
80
+ needle = f'"{ packagename } ":'
81
+ lines = raw_text .splitlines ()
82
+ for i , line in enumerate (lines , start = 1 ):
83
+ if needle in line :
84
+ return i , line .strip ()
85
+ return 1 , f'"{ packagename } ": declaration not found'
86
+ except FileNotFoundError :
87
+ return 1 , f"{ manifest_file } not found"
88
+ except Exception as e :
89
+ return 1 , f"Error reading { manifest_file } : { e } "
90
+
91
+ # For text-based files, define regex search patterns for common manifest types.
110
92
search_patterns = {
111
- "package.json" : rf'"{ packagename } ":\s*"{ packageversion } "' ,
112
93
"yarn.lock" : rf'{ packagename } @{ packageversion } ' ,
113
94
"pnpm-lock.yaml" : rf'"{ re .escape (packagename )} "\s*:\s*\{{[^}}]*"version":\s*"{ re .escape (packageversion )} "' ,
114
95
"requirements.txt" : rf'^{ re .escape (packagename )} \s*(?:==|===|!=|>=|<=|~=|\s+)?\s*{ re .escape (packageversion )} (?:\s*;.*)?$' ,
@@ -132,33 +113,25 @@ def find_line_in_file(packagename: str, packageversion: str, manifest_file: str)
132
113
"conanfile.txt" : rf'{ re .escape (packagename )} /{ re .escape (packageversion )} ' ,
133
114
"vcpkg.json" : rf'"{ re .escape (packagename )} ":\s*"{ re .escape (packageversion )} "' ,
134
115
}
135
-
136
- # If no specific pattern is found for this file name, fallback to a naive approach
137
116
searchstring = search_patterns .get (file_type , rf'{ re .escape (packagename )} .*{ re .escape (packageversion )} ' )
138
-
139
117
try :
140
- # Read file lines and search for a match
141
118
with open (manifest_file , 'r' , encoding = "utf-8" ) as file :
142
119
lines = [line .rstrip ("\n " ) for line in file ]
143
120
for line_number , line_content in enumerate (lines , start = 1 ):
144
- # For Python conditional dependencies, ignore everything after first ';'
121
+ # For cases where dependencies have conditionals (e.g. Python), only consider the main part.
145
122
line_main = line_content .split (";" , 1 )[0 ].strip ()
146
-
147
- # Use a case-insensitive regex search
148
123
if re .search (searchstring , line_main , re .IGNORECASE ):
149
124
return line_number , line_content .strip ()
150
-
151
125
except FileNotFoundError :
152
126
return 1 , f"{ manifest_file } not found"
153
127
except Exception as e :
154
128
return 1 , f"Error reading { manifest_file } : { e } "
155
-
156
129
return 1 , f"{ packagename } { packageversion } (not found)"
157
130
158
131
@staticmethod
159
132
def get_manifest_type_url (manifest_file : str , pkg_name : str , pkg_version : str ) -> str :
160
133
"""
161
- Determine the correct URL path based on the manifest file type .
134
+ Determine the URL prefix based on the manifest file.
162
135
"""
163
136
manifest_to_url_prefix = {
164
137
"package.json" : "npm" ,
@@ -181,18 +154,20 @@ def get_manifest_type_url(manifest_file: str, pkg_name: str, pkg_version: str) -
181
154
"composer.json" : "composer" ,
182
155
"vcpkg.json" : "vcpkg" ,
183
156
}
184
-
185
157
file_type = Path (manifest_file ).name
186
158
url_prefix = manifest_to_url_prefix .get (file_type , "unknown" )
187
159
return f"https://socket.dev/{ url_prefix } /package/{ pkg_name } /alerts/{ pkg_version } "
188
160
189
161
@staticmethod
190
162
def create_security_comment_sarif (diff ) -> dict :
191
163
"""
192
- Create SARIF-compliant output from the diff report, including dynamic URL generation
193
- based on manifest type and improved <br/> formatting for GitHub SARIF display.
164
+ Create a SARIF-compliant JSON object for alerts. This function now:
165
+ - Accepts multiple manifest files (from alert.introduced_by or alert.manifests)
166
+ - Generates one SARIF location per manifest file.
167
+ - Supports various language-specific manifest types.
194
168
"""
195
169
scan_failed = False
170
+ # (Optional: handle scan failure based on alert.error flags)
196
171
if len (diff .new_alerts ) == 0 :
197
172
for alert in diff .new_alerts :
198
173
if alert .error :
@@ -225,27 +200,30 @@ def create_security_comment_sarif(diff) -> dict:
225
200
rule_id = f"{ pkg_name } =={ pkg_version } "
226
201
severity = alert .severity
227
202
228
- # --- NEW LOGIC: Determine the list of manifest files ---
229
- if alert .introduced_by and isinstance (alert .introduced_by [0 ], list ):
230
- # Extract file names from each introduced_by entry
231
- manifest_files = [entry [1 ] for entry in alert .introduced_by ]
232
- elif alert .manifests :
233
- # Split semicolon-delimited manifest string if necessary
234
- manifest_files = [mf .strip () for mf in alert .manifests .split (";" )]
203
+ # --- Determine manifest files from alert data ---
204
+ manifest_files = []
205
+ if alert .introduced_by and isinstance (alert .introduced_by , list ):
206
+ for entry in alert .introduced_by :
207
+ if isinstance (entry , list ) and len (entry ) >= 2 :
208
+ manifest_files .append (entry [1 ])
209
+ elif isinstance (entry , str ):
210
+ manifest_files .extend ([m .strip () for m in entry .split (";" ) if m .strip ()])
211
+ elif hasattr (alert , 'manifests' ) and alert .manifests :
212
+ manifest_files = [mf .strip () for mf in alert .manifests .split (";" ) if mf .strip ()]
235
213
else :
236
214
manifest_files = ["requirements.txt" ]
237
215
238
- # Use the first file for generating the help URL.
216
+ # Use the first manifest for URL generation .
239
217
socket_url = Messages .get_manifest_type_url (manifest_files [0 ], pkg_name , pkg_version )
240
218
241
- # Prepare the description messages .
219
+ # Prepare descriptions with HTML <br/> for GitHub display .
242
220
short_desc = (
243
221
f"{ alert .props .get ('note' , '' )} <br/><br/>Suggested Action:<br/>"
244
222
f"{ alert .suggestion } <br/><a href=\" { socket_url } \" >{ socket_url } </a>"
245
223
)
246
224
full_desc = "{} - {}" .format (alert .title , alert .description .replace ('\r \n ' , '<br/>' ))
247
225
248
- # Create the rule if not already defined .
226
+ # Create or reuse the rule definition .
249
227
if rule_id not in rules_map :
250
228
rules_map [rule_id ] = {
251
229
"id" : rule_id ,
@@ -258,12 +236,12 @@ def create_security_comment_sarif(diff) -> dict:
258
236
},
259
237
}
260
238
261
- # --- NEW LOGIC: Create separate locations for each manifest file ---
239
+ # --- Build SARIF locations for each manifest file ---
262
240
locations = []
263
241
for mf in manifest_files :
264
242
line_number , line_content = Messages .find_line_in_file (pkg_name , pkg_version , mf )
265
243
if line_number < 1 :
266
- line_number = 1 # Ensure SARIF compliance.
244
+ line_number = 1
267
245
locations .append ({
268
246
"physicalLocation" : {
269
247
"artifactLocation" : {"uri" : mf },
@@ -274,15 +252,13 @@ def create_security_comment_sarif(diff) -> dict:
274
252
}
275
253
})
276
254
277
- # Add the SARIF result.
278
255
result_obj = {
279
256
"ruleId" : rule_id ,
280
257
"message" : {"text" : short_desc },
281
258
"locations" : locations ,
282
259
}
283
260
results_list .append (result_obj )
284
261
285
- # Attach rules and results.
286
262
sarif_data ["runs" ][0 ]["tool" ]["driver" ]["rules" ] = list (rules_map .values ())
287
263
sarif_data ["runs" ][0 ]["results" ] = results_list
288
264
0 commit comments