@@ -30,13 +30,16 @@ def map_severity_to_sarif(severity: str) -> str:
30
30
def find_line_in_file (packagename : str , packageversion : str , manifest_file : str ) -> tuple :
31
31
"""
32
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.
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
37
"""
37
38
file_type = Path (manifest_file ).name
38
39
39
- # Handle JSON-based files.
40
+ # --------------------
41
+ # 1) JSON-based manifests
42
+ # --------------------
40
43
if file_type in ["package-lock.json" , "Pipfile.lock" , "composer.lock" , "package.json" ]:
41
44
try :
42
45
with open (manifest_file , "r" , encoding = "utf-8" ) as f :
@@ -47,27 +50,26 @@ def find_line_in_file(packagename: str, packageversion: str, manifest_file: str)
47
50
data = {}
48
51
49
52
found = False
50
- # For package.json, check dependencies and devDependencies.
53
+ # For package.json, check both dependencies and devDependencies.
51
54
if file_type == "package.json" :
52
55
deps = data .get ("dependencies" , {})
53
56
deps_dev = data .get ("devDependencies" , {})
54
57
all_deps = {** deps , ** deps_dev }
55
58
if packagename in all_deps :
56
- actual_version = all_deps [packagename ]
57
59
# Allow for versions with caret/tilde prefixes.
60
+ actual_version = all_deps [packagename ]
58
61
if actual_version == packageversion or actual_version .lstrip ("^~" ) == packageversion :
59
62
found = True
60
63
else :
61
- # For other JSON-based manifests , look into common keys.
64
+ # For package-lock.json and similar , look into common keys.
62
65
for key in ["packages" , "default" , "dependencies" ]:
63
66
if key in data :
64
67
packages_dict = data [key ]
65
- # In package-lock.json, keys can be paths (e.g. "node_modules/axios")
68
+ # Keys in package-lock.json can be "node_modules/<pkg>"
66
69
for key_item , info in packages_dict .items ():
67
70
if key_item .endswith (packagename ):
68
- # info may be a dict (with "version") or a simple version string.
69
71
ver = info if isinstance (info , str ) else info .get ("version" , "" )
70
- if ver == packageversion :
72
+ if ver == packageversion or ver . lstrip ( "^~" ) == packageversion :
71
73
found = True
72
74
break
73
75
if found :
@@ -76,19 +78,31 @@ def find_line_in_file(packagename: str, packageversion: str, manifest_file: str)
76
78
if not found :
77
79
return 1 , f'"{ packagename } ": not found in { manifest_file } '
78
80
79
- # Now search the raw text to locate the declaration line.
80
- needle = f'"{ packagename } ":'
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 } "' )
87
+ else :
88
+ needles .append (f'"{ packagename } "' )
89
+
90
+ # Scan through the file's lines to locate a matching needle.
81
91
lines = raw_text .splitlines ()
82
92
for i , line in enumerate (lines , start = 1 ):
83
- if needle in line :
84
- return i , line .strip ()
93
+ for needle in needles :
94
+ if needle in line :
95
+ return i , line .strip ()
85
96
return 1 , f'"{ packagename } ": declaration not found'
86
97
except FileNotFoundError :
87
98
return 1 , f"{ manifest_file } not found"
88
99
except Exception as e :
89
100
return 1 , f"Error reading { manifest_file } : { e } "
90
101
91
- # For text-based files, define regex search patterns for common manifest types.
102
+ # --------------------
103
+ # 2) Text-based / line-based manifests
104
+ # --------------------
105
+ # Define regex patterns for common text-based manifest types.
92
106
search_patterns = {
93
107
"yarn.lock" : rf'{ packagename } @{ packageversion } ' ,
94
108
"pnpm-lock.yaml" : rf'"{ re .escape (packagename )} "\s*:\s*\{{[^}}]*"version":\s*"{ re .escape (packageversion )} "' ,
@@ -118,7 +132,6 @@ def find_line_in_file(packagename: str, packageversion: str, manifest_file: str)
118
132
with open (manifest_file , 'r' , encoding = "utf-8" ) as file :
119
133
lines = [line .rstrip ("\n " ) for line in file ]
120
134
for line_number , line_content in enumerate (lines , start = 1 ):
121
- # For cases where dependencies have conditionals (e.g. Python), only consider the main part.
122
135
line_main = line_content .split (";" , 1 )[0 ].strip ()
123
136
if re .search (searchstring , line_main , re .IGNORECASE ):
124
137
return line_number , line_content .strip ()
@@ -166,12 +179,10 @@ def create_security_comment_sarif(diff) -> dict:
166
179
- Generates one SARIF location per manifest file.
167
180
- Supports various language-specific manifest types.
168
181
"""
169
- scan_failed = False
170
182
# (Optional: handle scan failure based on alert.error flags)
171
183
if len (diff .new_alerts ) == 0 :
172
184
for alert in diff .new_alerts :
173
185
if alert .error :
174
- scan_failed = True
175
186
break
176
187
177
188
sarif_data = {
@@ -216,14 +227,12 @@ def create_security_comment_sarif(diff) -> dict:
216
227
# Use the first manifest for URL generation.
217
228
socket_url = Messages .get_manifest_type_url (manifest_files [0 ], pkg_name , pkg_version )
218
229
219
- # Prepare descriptions with HTML <br/> for GitHub display.
220
230
short_desc = (
221
231
f"{ alert .props .get ('note' , '' )} <br/><br/>Suggested Action:<br/>"
222
232
f"{ alert .suggestion } <br/><a href=\" { socket_url } \" >{ socket_url } </a>"
223
233
)
224
234
full_desc = "{} - {}" .format (alert .title , alert .description .replace ('\r \n ' , '<br/>' ))
225
235
226
- # Create or reuse the rule definition.
227
236
if rule_id not in rules_map :
228
237
rules_map [rule_id ] = {
229
238
"id" : rule_id ,
@@ -236,7 +245,6 @@ def create_security_comment_sarif(diff) -> dict:
236
245
},
237
246
}
238
247
239
- # --- Build SARIF locations for each manifest file ---
240
248
locations = []
241
249
for mf in manifest_files :
242
250
line_number , line_content = Messages .find_line_in_file (pkg_name , pkg_version , mf )
@@ -263,7 +271,7 @@ def create_security_comment_sarif(diff) -> dict:
263
271
sarif_data ["runs" ][0 ]["results" ] = results_list
264
272
265
273
return sarif_data
266
-
274
+
267
275
@staticmethod
268
276
def create_security_comment_json (diff : Diff ) -> dict :
269
277
scan_failed = False
0 commit comments