fosslight-dependency 4.1.25__py3-none-any.whl → 4.1.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@
6
6
  import os
7
7
  import logging
8
8
  import fosslight_dependency.constant as const
9
+ from fosslight_dependency._package_manager import deduplicate_dep_items
9
10
  from fosslight_dependency.package_manager.Pypi import Pypi
10
11
  from fosslight_dependency.package_manager.Npm import Npm
11
12
  from fosslight_dependency.package_manager.Yarn import Yarn
@@ -104,16 +105,19 @@ def analyze_dependency(package_manager_name, input_dir, output_dir, pip_activate
104
105
  for f_name in package_manager.input_package_list_file:
105
106
  logger.info(f"Parse oss information with file: {f_name}")
106
107
 
107
- if os.path.isfile(f_name):
108
+ file_path = os.path.join(input_dir, f_name) if not os.path.isabs(f_name) else f_name
109
+ if os.path.isfile(file_path):
108
110
  package_manager.parse_oss_information(f_name)
109
111
  package_dep_item_list.extend(package_manager.dep_items)
110
112
  else:
111
- logger.error(f"Failed to open input file: {f_name}")
113
+ logger.error(f"Failed to open input file: {file_path}")
112
114
  ret = False
113
115
  if package_manager_name == const.PNPM:
114
116
  logger.info("Parse oss information for pnpm")
115
117
  package_manager.parse_oss_information_for_pnpm()
116
118
  package_dep_item_list.extend(package_manager.dep_items)
119
+ if package_dep_item_list:
120
+ package_dep_item_list = deduplicate_dep_items(package_dep_item_list)
117
121
  if ret:
118
122
  logger.warning(f"### Complete to analyze: {package_manager_name}({input_dir}: {','.join(manifest_file_name)})")
119
123
  if package_manager.cover_comment:
@@ -37,7 +37,9 @@ _HELP_MESSAGE_DEPENDENCY = f"""
37
37
  \t(npm, maven, gradle, pypi, pub, cocoapods, android, swift, carthage,
38
38
  \t go, nuget, helm, unity, cargo, pnpm, yarn)
39
39
  -p <input_path>\t\t Enter the path where the script will be run.
40
- -e <exclude_path>\t\t Enter the path where the analysis will not be performed.
40
+ -e <exclude_path>\t\t Enter the path where the analysis will not be performed (files and directories).
41
+ \t\t\t\t * IMPORTANT: Always wrap patterns in double quotes ("") to avoid shell expansion.
42
+ \t\t\t\t Example) fosslight_dependency -e "test/abc.py" "*.jar"
41
43
  -o <output_path>\t\t Output path
42
44
  \t\t\t\t\t(If you want to generate the specific file name, add the output path with file name.)
43
45
  -f <format> [<format> ...]\t Output formats
@@ -369,3 +369,29 @@ def change_file_mode(filepath, mode=''):
369
369
  os.chmod(filepath, new_mode)
370
370
  logger.debug(f"File mode of {filepath} has been changed to {oct(new_mode)}.")
371
371
  return current_mode
372
+
373
+
374
+ def deduplicate_dep_items(dep_items):
375
+ if not dep_items:
376
+ return dep_items
377
+
378
+ unique_items = []
379
+ seen = set()
380
+
381
+ for item in dep_items:
382
+ first_oss = item.oss_items[0] if getattr(item, "oss_items", None) else None
383
+ oss_name = getattr(first_oss, "name", None) if first_oss else None
384
+ oss_ver = getattr(first_oss, "version", None) if first_oss else None
385
+ comment = getattr(first_oss, "comment", None) if first_oss else None
386
+
387
+ depends_on = None
388
+ if getattr(item, "depends_on", None):
389
+ depends_on = tuple(sorted(item.depends_on))
390
+
391
+ key = (getattr(item, "purl", None), oss_name, oss_ver, comment, depends_on)
392
+ if key in seen:
393
+ continue
394
+ seen.add(key)
395
+ unique_items.append(item)
396
+
397
+ return unique_items
@@ -41,7 +41,7 @@ SUPPORT_PACKAE = {
41
41
  SWIFT: 'Package.resolved',
42
42
  CARTHAGE: 'Cartfile.resolved',
43
43
  GO: 'go.mod',
44
- NUGET: ['packages.config', os.path.join('obj', 'project.assets.json')],
44
+ NUGET: ['packages.config', os.path.join('obj', 'project.assets.json'), 'Directory.Packages.props'],
45
45
  HELM: 'Chart.yaml',
46
46
  UNITY: os.path.join('Library', 'PackageManager', 'ProjectCache'),
47
47
  CARGO: 'Cargo.toml'
@@ -255,10 +255,11 @@ class Maven(PackageManager):
255
255
  dep_key = f"{oss_item.name}({version})"
256
256
 
257
257
  if self.direct_dep:
258
- if dep_key in self.direct_dep_list:
259
- oss_item.comment = 'direct'
260
- else:
261
- oss_item.comment = 'transitive'
258
+ if len(self.direct_dep_list) > 0:
259
+ if dep_key in self.direct_dep_list:
260
+ oss_item.comment = 'direct'
261
+ else:
262
+ oss_item.comment = 'transitive'
262
263
  try:
263
264
  if dep_key in self.relation_tree:
264
265
  dep_item.depends_on_raw = self.relation_tree[dep_key]
@@ -9,6 +9,7 @@ import subprocess
9
9
  import json
10
10
  import shutil
11
11
  import re
12
+ import requests
12
13
  import fosslight_util.constant as constant
13
14
  import fosslight_dependency.constant as const
14
15
  from fosslight_dependency._package_manager import PackageManager, get_url_to_purl
@@ -26,9 +27,11 @@ class Npm(PackageManager):
26
27
  input_file_name = 'tmp_npm_license_output.json'
27
28
  tmp_custom_json = 'custom.json'
28
29
  flag_tmp_node_modules = False
30
+ _network_available = False
29
31
 
30
32
  def __init__(self, input_dir, output_dir):
31
33
  super().__init__(self.package_manager_name, self.dn_url, input_dir, output_dir)
34
+ self._check_network_available()
32
35
 
33
36
  def __del__(self):
34
37
  if os.path.isfile(os.path.join(self.input_dir, self.input_file_name)):
@@ -174,24 +177,33 @@ class Npm(PackageManager):
174
177
  package_path = d['path']
175
178
 
176
179
  private_pkg = False
177
- if _private in d:
178
- if d[_private]:
179
- private_pkg = True
180
+ if _private in d and d[_private]:
181
+ private_pkg = True
180
182
 
181
- oss_item.download_location = f"{self.dn_url}{oss_init_name}/v/{oss_item.version}"
182
- dn_loc = f"{self.dn_url}{oss_init_name}"
183
- dep_item.purl = get_url_to_purl(oss_item.download_location, self.package_manager_name)
184
- purl_dict[f'{oss_init_name}({oss_item.version})'] = dep_item.purl
185
- if d[_repository]:
186
- dn_loc = d[_repository]
187
- elif private_pkg:
188
- dn_loc = ''
183
+ oss_item.download_location = ''
189
184
 
190
- oss_item.homepage = dn_loc
185
+ npm_dl_url = f"{self.dn_url}{oss_init_name}/v/{oss_item.version}"
186
+ npm_home_url = f"{self.dn_url}{oss_init_name}"
187
+ dep_item.purl = get_url_to_purl(npm_dl_url, self.package_manager_name)
188
+ purl_dict[f'{oss_init_name}({oss_item.version})'] = dep_item.purl
191
189
 
190
+ repo_url = d[_repository] if d[_repository] else ''
192
191
  if private_pkg:
192
+ oss_item.homepage = repo_url or ''
193
193
  oss_item.download_location = oss_item.homepage
194
194
  oss_item.comment = 'private'
195
+ else:
196
+ npm_url_exists = False
197
+ if self._network_available is True:
198
+ npm_url_exists = self._npm_url_exists(oss_init_name)
199
+
200
+ if self._network_available and not npm_url_exists:
201
+ oss_item.homepage = repo_url or ""
202
+ oss_item.download_location = oss_item.homepage
203
+ else:
204
+ oss_item.homepage = repo_url or npm_home_url
205
+ oss_item.download_location = npm_dl_url
206
+
195
207
  if self.package_name == f'{oss_init_name}({oss_item.version})':
196
208
  oss_item.comment = 'root package'
197
209
  elif self.direct_dep and len(self.relation_tree) > 0:
@@ -221,6 +233,25 @@ class Npm(PackageManager):
221
233
  self.dep_items = change_dependson_to_purl(purl_dict, self.dep_items)
222
234
  return
223
235
 
236
+ def _check_network_available(self) -> bool:
237
+ test_url = 'https://registry.npmjs.org/'
238
+ try:
239
+ resp = requests.head(test_url, timeout=3, allow_redirects=True)
240
+ self._network_available = resp.status_code < 400
241
+ except Exception:
242
+ self._network_available = False
243
+ return self._network_available
244
+
245
+ def _npm_url_exists(self, package_name: str) -> bool:
246
+ url = f"https://registry.npmjs.org/{package_name}"
247
+ try:
248
+ resp = requests.head(url, timeout=3, allow_redirects=True)
249
+ if resp.status_code == 405:
250
+ resp = requests.get(url, timeout=3, allow_redirects=True)
251
+ return resp.status_code < 400
252
+ except Exception:
253
+ return False
254
+
224
255
 
225
256
  def check_multi_license(license_name, manifest_file_path):
226
257
  multi_license_list = []
@@ -6,6 +6,7 @@
6
6
  import logging
7
7
  import re
8
8
  import os
9
+ import subprocess
9
10
  from defusedxml.ElementTree import parse, fromstring
10
11
  import json
11
12
  import requests
@@ -24,30 +25,151 @@ class Nuget(PackageManager):
24
25
 
25
26
  dn_url = "https://nuget.org/packages/"
26
27
  packageReference = False
28
+ directory_packages_props = 'Directory.Packages.props'
27
29
  nuget_api_url = 'https://api.nuget.org/v3-flatcontainer/'
28
30
  dotnet_ver = []
31
+ _exclude_dirs = {"test", "tests", "sample", "samples", "example", "examples"}
29
32
 
30
33
  def __init__(self, input_dir, output_dir):
31
34
  super().__init__(self.package_manager_name, self.dn_url, input_dir, output_dir)
32
35
 
33
36
  for manifest_i in const.SUPPORT_PACKAE.get(self.package_manager_name):
34
- if os.path.isfile(manifest_i):
35
- self.append_input_package_list_file(manifest_i)
37
+ if os.path.exists(os.path.basename(manifest_i)):
38
+ self.append_input_package_list_file(os.path.basename(manifest_i))
36
39
  if manifest_i != 'packages.config':
37
40
  self.packageReference = True
38
41
 
42
+ def run_plugin(self):
43
+ ret = True
44
+ directory_packages_props_path = os.path.join(self.input_dir, self.directory_packages_props)
45
+ if not os.path.isfile(directory_packages_props_path):
46
+ return ret
47
+
48
+ logger.info(f"Found {self.directory_packages_props}. Using NuGet CPM flow.")
49
+ self.packageReference = True
50
+
51
+ has_any_assets = False
52
+ for root, dirs, files in os.walk(self.input_dir):
53
+ rel_root = os.path.relpath(root, self.input_dir)
54
+ parts = rel_root.split(os.sep) if rel_root != os.curdir else []
55
+ if any(p.lower() in self._exclude_dirs for p in parts):
56
+ continue
57
+ assets_candidate = os.path.join(root, 'obj', 'project.assets.json')
58
+ if os.path.isfile(assets_candidate):
59
+ has_any_assets = True
60
+ break
61
+
62
+ if not has_any_assets:
63
+ logger.info("No project.assets.json found. Running 'dotnet restore'...")
64
+ restore_targets = self._find_restore_targets()
65
+ if not restore_targets:
66
+ logger.warning("No .sln or .csproj files found to restore.")
67
+ return False
68
+
69
+ success = False
70
+ for target_path, target_file in restore_targets:
71
+ logger.info(f"Restoring: {os.path.relpath(target_file, self.input_dir)}")
72
+ try:
73
+ result = subprocess.run(
74
+ ['dotnet', 'restore', target_file, '/p:EnableWindowsTargeting=true'],
75
+ cwd=target_path,
76
+ capture_output=True,
77
+ text=True,
78
+ timeout=300
79
+ )
80
+ if result.returncode == 0:
81
+ success = True
82
+ else:
83
+ logger.warning(f"'dotnet restore' failed for {target_file} with return code {result.returncode}")
84
+ if result.stderr:
85
+ logger.warning(result.stderr)
86
+ except FileNotFoundError:
87
+ logger.error("'dotnet' command not found. Please install .NET SDK.")
88
+ return False
89
+ except subprocess.TimeoutExpired:
90
+ logger.warning(f"'dotnet restore' timed out for {target_file}.")
91
+ except Exception as e:
92
+ logger.warning(f"Failed to run 'dotnet restore' for {target_file}: {e}")
93
+
94
+ if not success:
95
+ logger.warning("All 'dotnet restore' attempts failed.")
96
+ return False
97
+
98
+ self.project_dirs = []
99
+ found_projects = False
100
+
101
+ for root, dirs, files in os.walk(self.input_dir):
102
+ rel_root = os.path.relpath(root, self.input_dir)
103
+ parts = rel_root.split(os.sep) if rel_root != os.curdir else []
104
+ if any(p.lower() in self._exclude_dirs for p in parts):
105
+ continue
106
+ assets_json = os.path.join(root, 'obj', 'project.assets.json')
107
+ if os.path.isfile(assets_json):
108
+ found_projects = True
109
+
110
+ rel_path = os.path.relpath(assets_json, self.input_dir)
111
+ logger.info(f"Found project.assets.json at: {rel_path}")
112
+
113
+ if rel_path not in self.input_package_list_file:
114
+ self.append_input_package_list_file(rel_path)
115
+
116
+ project_dir = os.path.dirname(assets_json)
117
+ if project_dir.endswith(os.sep + 'obj'):
118
+ project_dir = project_dir[: -len(os.sep + 'obj')]
119
+
120
+ if project_dir and project_dir not in self.project_dirs:
121
+ self.project_dirs.append(project_dir)
122
+
123
+ if not found_projects:
124
+ logger.warning(
125
+ "Directory.Packages.props found and 'dotnet restore' completed, "
126
+ "but no obj/project.assets.json files were discovered."
127
+ )
128
+
129
+ return ret
130
+
39
131
  def parse_oss_information(self, f_name):
40
132
  tmp_license_txt_file_name = 'tmp_license.txt'
41
- with open(f_name, 'r', encoding='utf8') as input_fp:
42
- purl_dict = {}
133
+ if f_name == self.directory_packages_props:
134
+ return
135
+
136
+ relation_tree = {}
137
+ direct_dep_list = []
138
+ if not hasattr(self, 'global_purl_dict'):
139
+ self.global_purl_dict = {}
140
+ if not hasattr(self, 'processed_packages'):
141
+ self.processed_packages = {}
142
+
143
+ file_path = os.path.join(self.input_dir, f_name) if not os.path.isabs(f_name) else f_name
144
+ with open(file_path, 'r', encoding='utf8') as input_fp:
43
145
  package_list = []
44
146
  if self.packageReference:
45
- package_list = self.get_package_info_in_packagereference(input_fp)
147
+ package_list = self.get_package_info_in_packagereference(input_fp, relation_tree, direct_dep_list)
46
148
  else:
47
149
  package_list = self.get_package_list_in_packages_config(input_fp)
48
150
 
49
151
  for oss_origin_name, oss_version in package_list:
50
152
  try:
153
+ pkg_key = f'{oss_origin_name}({oss_version})'
154
+ if pkg_key in self.processed_packages:
155
+ existing_idx = self.processed_packages[pkg_key]
156
+ existing_dep_item = self.dep_items[existing_idx]
157
+
158
+ if pkg_key in relation_tree:
159
+ new_deps = relation_tree[pkg_key]
160
+ if existing_dep_item.depends_on_raw:
161
+ existing_deps_set = set(existing_dep_item.depends_on_raw)
162
+ new_deps_set = set(new_deps)
163
+ merged_deps = sorted(existing_deps_set | new_deps_set)
164
+ existing_dep_item.depends_on_raw = merged_deps
165
+ else:
166
+ existing_dep_item.depends_on_raw = new_deps
167
+ if self.direct_dep and self.packageReference:
168
+ if oss_origin_name in direct_dep_list:
169
+ if 'direct' not in existing_dep_item.oss_items[0].comment:
170
+ existing_dep_item.oss_items[0].comment = 'direct'
171
+ continue
172
+
51
173
  dep_item = DependencyItem()
52
174
  oss_item = OssItem()
53
175
  oss_item.name = f'{self.package_manager_name}:{oss_origin_name}'
@@ -88,7 +210,7 @@ class Nuget(PackageManager):
88
210
  if proj_url_id is not None:
89
211
  oss_item.download_location = proj_url_id.text
90
212
  oss_item.homepage = f'{self.dn_url}{oss_origin_name}'
91
- if oss_item.download_location == '':
213
+ if not oss_item.download_location:
92
214
  oss_item.download_location = f'{oss_item.homepage}/{oss_item.version}'
93
215
  else:
94
216
  if oss_item.download_location.endswith('.git'):
@@ -97,24 +219,26 @@ class Nuget(PackageManager):
97
219
  else:
98
220
  oss_item.comment = 'Fail to response for nuget api'
99
221
  dep_item.purl = f'pkg:nuget/{oss_origin_name}@{oss_item.version}'
100
- purl_dict[f'{oss_origin_name}({oss_item.version})'] = dep_item.purl
222
+ self.global_purl_dict[f'{oss_origin_name}({oss_item.version})'] = dep_item.purl
101
223
 
102
224
  if self.direct_dep and self.packageReference:
103
- if oss_origin_name in self.direct_dep_list:
225
+ if oss_origin_name in direct_dep_list:
104
226
  oss_item.comment = 'direct'
105
227
  else:
106
228
  oss_item.comment = 'transitive'
107
229
 
108
- if f'{oss_origin_name}({oss_item.version})' in self.relation_tree:
109
- dep_item.depends_on_raw = self.relation_tree[f'{oss_origin_name}({oss_item.version})']
230
+ key = f'{oss_origin_name}({oss_item.version})'
231
+ if key in relation_tree:
232
+ dep_item.depends_on_raw = relation_tree[key]
110
233
 
111
234
  dep_item.oss_items.append(oss_item)
112
235
  self.dep_items.append(dep_item)
236
+ self.processed_packages[pkg_key] = len(self.dep_items) - 1
113
237
 
114
238
  except Exception as e:
115
239
  logger.warning(f"Failed to parse oss information: {e}")
116
240
  if self.direct_dep:
117
- self.dep_items = change_dependson_to_purl(purl_dict, self.dep_items)
241
+ self.dep_items = change_dependson_to_purl(self.global_purl_dict, self.dep_items)
118
242
 
119
243
  if os.path.isfile(tmp_license_txt_file_name):
120
244
  os.remove(tmp_license_txt_file_name)
@@ -128,13 +252,14 @@ class Nuget(PackageManager):
128
252
  package_list.append([p.get("id"), p.get("version")])
129
253
  return package_list
130
254
 
131
- def get_package_info_in_packagereference(self, input_fp):
255
+ def get_package_info_in_packagereference(self, input_fp, relation_tree, direct_dep_list):
132
256
  json_f = json.load(input_fp)
133
257
 
134
- self.get_dotnet_ver_list(json_f)
258
+ dotnet_ver = self.get_dotnet_ver_list(json_f)
135
259
  package_list = self.get_package_list_in_packages_assets(json_f)
136
- self.get_dependency_tree(json_f)
137
- self.get_direct_package_in_packagereference()
260
+ self.get_dependency_tree(json_f, relation_tree, dotnet_ver)
261
+ self.get_direct_dependencies_from_assets_json(json_f, direct_dep_list)
262
+ self.get_direct_package_in_packagereference(direct_dep_list)
138
263
 
139
264
  return package_list
140
265
 
@@ -147,14 +272,35 @@ class Nuget(PackageManager):
147
272
  return package_list
148
273
 
149
274
  def get_dotnet_ver_list(self, json_f):
275
+ dotnet_ver = []
150
276
  json_project_group = json_f['projectFileDependencyGroups']
151
- for dotnet_ver in json_project_group:
152
- self.dotnet_ver.append(dotnet_ver)
277
+ for ver in json_project_group:
278
+ dotnet_ver.append(ver)
279
+ return dotnet_ver
280
+
281
+ def get_direct_dependencies_from_assets_json(self, json_f, direct_dep_list):
282
+ try:
283
+ json_project_group = json_f.get('projectFileDependencyGroups', {})
284
+ for _, dependencies in json_project_group.items():
285
+ if not dependencies:
286
+ continue
287
+ for dep_string in dependencies:
288
+ package_name = dep_string.split()[0] if dep_string else ''
289
+ if package_name and package_name not in direct_dep_list:
290
+ direct_dep_list.append(package_name)
291
+ except Exception as e:
292
+ logger.warning(f"Failed to extract direct dependencies from project.assets.json: {e}")
293
+
294
+ def get_dependency_tree(self, json_f, relation_tree, dotnet_ver):
295
+ actual_versions = {}
296
+ for lib_key in json_f.get('libraries', {}):
297
+ if '/' in lib_key:
298
+ lib_name, lib_version = lib_key.split('/', 1)
299
+ actual_versions[lib_name.lower()] = lib_version
153
300
 
154
- def get_dependency_tree(self, json_f):
155
301
  json_target = json_f['targets']
156
302
  for item in json_target:
157
- if item not in self.dotnet_ver:
303
+ if item not in dotnet_ver:
158
304
  continue
159
305
  json_item = json_target[item]
160
306
  for pkg in json_item:
@@ -166,21 +312,39 @@ class Nuget(PackageManager):
166
312
  if json_pkg['type'] != 'package':
167
313
  continue
168
314
  oss_info = pkg.split('/')
169
- self.relation_tree[f'{oss_info[0]}({oss_info[1]})'] = []
315
+ relation_tree[f'{oss_info[0]}({oss_info[1]})'] = []
170
316
  for dep in json_pkg['dependencies']:
171
317
  oss_name = dep
172
- oss_ver = json_pkg['dependencies'][dep]
173
- self.relation_tree[f'{oss_info[0]}({oss_info[1]})'].append(f'{oss_name}({oss_ver})')
174
-
175
- def get_direct_package_in_packagereference(self):
176
- for f in os.listdir(self.input_dir):
177
- if os.path.isfile(f) and ((f.split('.')[-1] == 'csproj') or (f.split('.')[-1] == 'xproj')):
178
- with open(f, 'r', encoding='utf8') as input_fp:
179
- root = parse(input_fp).getroot()
180
- itemgroups = root.findall('ItemGroup')
181
- for itemgroup in itemgroups:
182
- for item in itemgroup.findall('PackageReference'):
183
- self.direct_dep_list.append(item.get('Include'))
318
+ dep_ver_in_spec = json_pkg['dependencies'][dep]
319
+ actual_ver = actual_versions.get(oss_name.lower(), dep_ver_in_spec)
320
+ relation_tree[f'{oss_info[0]}({oss_info[1]})'].append(f'{oss_name}({actual_ver})')
321
+
322
+ def get_direct_package_in_packagereference(self, direct_dep_list):
323
+ if hasattr(self, 'project_dirs') and self.project_dirs:
324
+ for project_dir in self.project_dirs:
325
+ for f in os.listdir(project_dir):
326
+ f_path = os.path.join(project_dir, f)
327
+ if os.path.isfile(f_path) and ((f.split('.')[-1] == 'csproj') or (f.split('.')[-1] == 'xproj')):
328
+ with open(f_path, 'r', encoding='utf8') as input_fp:
329
+ root = parse(input_fp).getroot()
330
+ itemgroups = root.findall('ItemGroup')
331
+ for itemgroup in itemgroups:
332
+ for item in itemgroup.findall('PackageReference'):
333
+ pkg_name = item.get('Include')
334
+ if pkg_name and pkg_name not in direct_dep_list:
335
+ direct_dep_list.append(pkg_name)
336
+ else:
337
+ for f in os.listdir(self.input_dir):
338
+ f_path = os.path.join(self.input_dir, f)
339
+ if os.path.isfile(f_path) and ((f.split('.')[-1] == 'csproj') or (f.split('.')[-1] == 'xproj')):
340
+ with open(f_path, 'r', encoding='utf8') as input_fp:
341
+ root = parse(input_fp).getroot()
342
+ itemgroups = root.findall('ItemGroup')
343
+ for itemgroup in itemgroups:
344
+ for item in itemgroup.findall('PackageReference'):
345
+ pkg_name = item.get('Include')
346
+ if pkg_name and pkg_name not in direct_dep_list:
347
+ direct_dep_list.append(pkg_name)
184
348
 
185
349
  def check_multi_license(self, license_name):
186
350
  multi_license = license_name
@@ -194,3 +358,30 @@ class Nuget(PackageManager):
194
358
  logger.warning(f'Fail to parse multi license in npm: {e}')
195
359
 
196
360
  return multi_license, license_comment
361
+
362
+ def _find_restore_targets(self):
363
+ sln_files = []
364
+ csproj_files = []
365
+
366
+ for root, dirs, files in os.walk(self.input_dir):
367
+ rel_root = os.path.relpath(root, self.input_dir)
368
+ parts = rel_root.split(os.sep) if rel_root != os.curdir else []
369
+ if any(p.lower() in self._exclude_dirs for p in parts):
370
+ continue
371
+
372
+ depth = len(parts) if parts and parts[0] != '.' else 0
373
+
374
+ for f in files:
375
+ if f.endswith('.sln'):
376
+ sln_files.append((depth, root, os.path.join(root, f)))
377
+ elif f.endswith('.csproj'):
378
+ csproj_files.append((depth, root, os.path.join(root, f)))
379
+
380
+ if sln_files:
381
+ sln_files.sort(key=lambda x: x[0])
382
+ min_depth = sln_files[0][0]
383
+ return [(d, f) for depth, d, f in sln_files if depth == min_depth]
384
+ if csproj_files:
385
+ return [(d, f) for _, d, f in csproj_files]
386
+
387
+ return []
@@ -121,24 +121,33 @@ class Yarn(Npm):
121
121
  package_path = d['path']
122
122
 
123
123
  private_pkg = False
124
- if _private in d:
125
- if d[_private]:
126
- private_pkg = True
124
+ if _private in d and d[_private]:
125
+ private_pkg = True
127
126
 
128
- oss_item.download_location = f"{self.dn_url}{oss_init_name}/v/{oss_item.version}"
129
- dn_loc = f"{self.dn_url}{oss_init_name}"
130
- dep_item.purl = get_url_to_purl(oss_item.download_location, self.package_manager_name)
131
- purl_dict[f'{oss_init_name}({oss_item.version})'] = dep_item.purl
132
- if d[_repository]:
133
- dn_loc = d[_repository]
134
- elif private_pkg:
135
- dn_loc = ''
127
+ oss_item.download_location = ''
136
128
 
137
- oss_item.homepage = dn_loc
129
+ npm_dl_url = f"{self.dn_url}{oss_init_name}/v/{oss_item.version}"
130
+ npm_home_url = f"{self.dn_url}{oss_init_name}"
131
+ dep_item.purl = get_url_to_purl(npm_dl_url, self.package_manager_name)
132
+ purl_dict[f'{oss_init_name}({oss_item.version})'] = dep_item.purl
138
133
 
134
+ repo_url = d[_repository] if d[_repository] else ''
139
135
  if private_pkg:
136
+ oss_item.homepage = repo_url or ''
140
137
  oss_item.download_location = oss_item.homepage
141
138
  oss_item.comment = 'private'
139
+ else:
140
+ npm_url_exists = False
141
+ if self._network_available is True:
142
+ npm_url_exists = self._npm_url_exists(oss_init_name, oss_item.version)
143
+
144
+ if self._network_available and not npm_url_exists:
145
+ oss_item.homepage = repo_url or ""
146
+ oss_item.download_location = oss_item.homepage
147
+ else:
148
+ oss_item.homepage = repo_url or npm_home_url
149
+ oss_item.download_location = npm_dl_url
150
+
142
151
  if self.package_name == f'{oss_init_name}({oss_item.version})':
143
152
  oss_item.comment = 'root package'
144
153
  elif self.direct_dep and len(self.relation_tree) > 0:
@@ -60,6 +60,7 @@ def find_package_manager(input_dir, abs_path_to_exclude=[], manifest_file_name=[
60
60
  manifest_file_name.append(value)
61
61
 
62
62
  found_manifest_file = []
63
+ found_manifest_set = set()
63
64
  suggested_files = []
64
65
  for parent, dirs, files in os.walk(input_dir):
65
66
  parent_parts = parent.split(os.sep)
@@ -74,17 +75,30 @@ def find_package_manager(input_dir, abs_path_to_exclude=[], manifest_file_name=[
74
75
  for exclude_path in abs_path_to_exclude):
75
76
  continue
76
77
  if file in manifest_file_name:
77
- path_with_filename = os.path.join(parent, file)
78
- found_manifest_file.append(path_with_filename)
78
+ candidate = os.path.join(parent, file)
79
+ norm_candidate = os.path.normpath(candidate)
80
+ if norm_candidate not in found_manifest_set:
81
+ found_manifest_set.add(norm_candidate)
82
+ found_manifest_file.append(candidate)
83
+ for manifest_f in manifest_file_name:
84
+ candidate = os.path.join(parent, manifest_f)
85
+ norm_candidate = os.path.normpath(candidate)
86
+ if os.path.exists(candidate) and norm_candidate not in found_manifest_set:
87
+ found_manifest_set.add(norm_candidate)
88
+ found_manifest_file.append(candidate)
79
89
  if file in const.SUGGESTED_PACKAGE.keys():
80
90
  suggested_files.append(os.path.join(parent, file))
91
+
81
92
  for dir in dirs:
82
93
  for manifest_f in manifest_file_name:
83
94
  manifest_l = manifest_f.split(os.path.sep)
84
- if len(manifest_l) > 1:
85
- if manifest_l[0] == dir:
86
- if os.path.exists(os.path.join(parent, manifest_f)):
87
- found_manifest_file.append(os.path.join(parent, manifest_f))
95
+ if len(manifest_l) > 1 and manifest_l[0] == dir:
96
+ candidate = os.path.join(parent, manifest_f)
97
+ norm_candidate = os.path.normpath(candidate)
98
+ if os.path.exists(candidate) and norm_candidate not in found_manifest_set:
99
+ found_manifest_set.add(norm_candidate)
100
+ found_manifest_file.append(candidate)
101
+
88
102
  if not recursive:
89
103
  if len(found_manifest_file) > 0:
90
104
  input_dir = parent
@@ -95,12 +109,22 @@ def find_package_manager(input_dir, abs_path_to_exclude=[], manifest_file_name=[
95
109
  f_name = os.path.basename(f_with_path)
96
110
  dir_path = os.path.dirname(f_with_path)
97
111
  for key, value in const.SUPPORT_PACKAE.items():
98
- if isinstance(value, list):
99
- if f_name in value:
100
- found_package_manager[key][dir_path].append(f_name)
101
- else:
102
- if f_name == value:
103
- found_package_manager[key][dir_path].append(f_name)
112
+ manifest_patterns = value if isinstance(value, list) else [value]
113
+
114
+ for pattern in manifest_patterns:
115
+ if os.path.sep not in pattern:
116
+ if f_name == pattern:
117
+ if pattern not in found_package_manager[key][dir_path]:
118
+ found_package_manager[key][dir_path].append(pattern)
119
+ else:
120
+ rel_dir, rel_file = os.path.split(pattern)
121
+ expected_path = os.path.join(dir_path, rel_file)
122
+
123
+ if f_name == rel_file:
124
+ candidate = os.path.join(os.path.dirname(dir_path), rel_dir, rel_file) if rel_dir else expected_path
125
+ if os.path.normpath(candidate) == os.path.normpath(f_with_path):
126
+ if pattern not in found_package_manager[key][dir_path]:
127
+ found_package_manager[key][dir_path].append(pattern)
104
128
  found_package_manager = {k: dict(v) for k, v in found_package_manager.items()}
105
129
 
106
130
  # both npm and pnpm are detected, remove npm.
@@ -133,10 +157,12 @@ def find_package_manager(input_dir, abs_path_to_exclude=[], manifest_file_name=[
133
157
  return ret, found_package_manager, input_dir, suggested_files
134
158
 
135
159
 
136
- def print_package_info(success_pm, log_lines):
137
- if success_pm:
138
- for pm, dir_dict in success_pm.items():
139
- log_lines.append(f"- {pm}:")
160
+ def print_package_info(pm, log_lines, status=''):
161
+ if pm:
162
+ if status:
163
+ status = f"[{status}] "
164
+ for pm, dir_dict in pm.items():
165
+ log_lines.append(f"- {status} {pm}:")
140
166
  for path, files in dir_dict.items():
141
167
  file_list = ', '.join(files)
142
168
  log_lines.append(f" {path}: {file_list}")
@@ -318,16 +344,14 @@ def run_dependency_scanner(package_manager='', input_dir='', output_dir_file='',
318
344
  success_pm = {k: dict(v) for k, v in success_pm.items()}
319
345
  fail_pm = {k: dict(v) for k, v in fail_pm.items()}
320
346
  if len(found_package_manager.keys()) > 0:
347
+ log_lines = ["Dependency Analysis Summary"]
321
348
  if len(success_pm) > 0:
322
- log_lines = ["Success to analyze:"]
323
- log_lines = print_package_info(success_pm, log_lines)
324
- scan_item.set_cover_comment('\n'.join(log_lines))
349
+ log_lines = print_package_info(success_pm, log_lines, 'Success')
325
350
  if len(fail_pm) > 0:
326
- log_lines = ["Fail to analyze:"]
327
- log_lines = print_package_info(fail_pm, log_lines)
328
- scan_item.set_cover_comment('\n'.join(log_lines))
329
- scan_item.set_cover_comment('Check log file(fosslight_log*.txt) '
330
- 'and https://fosslight.org/fosslight-guide-en/scanner/3_dependency.html#-prerequisite.')
351
+ log_lines = print_package_info(fail_pm, log_lines, 'Fail')
352
+ log_lines.append('If analysis fails, see fosslight_log*.txt and the prerequisite guide: '
353
+ 'https://fosslight.org/fosslight-guide-en/scanner/3_dependency.html#-prerequisite.')
354
+ scan_item.set_cover_comment('\n'.join(log_lines))
331
355
 
332
356
  if ret and graph_path:
333
357
  graph_path = os.path.abspath(graph_path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fosslight_dependency
3
- Version: 4.1.25
3
+ Version: 4.1.26
4
4
  Summary: FOSSLight Dependency Scanner
5
5
  Home-page: https://github.com/fosslight/fosslight_dependency_scanner
6
6
  Download-URL: https://github.com/fosslight/fosslight_dependency_scanner
@@ -1,11 +1,11 @@
1
1
  fosslight_dependency/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- fosslight_dependency/_analyze_dependency.py,sha256=TX86cl7BF3_kCsAQY459X9sqGqP0xbJI7hqnqe7IbnU,5598
2
+ fosslight_dependency/_analyze_dependency.py,sha256=-iETqo4DNu_d8iJtC7Of0waY6YHjJ8OB8Lf_m-9WZXw,5888
3
3
  fosslight_dependency/_graph_convertor.py,sha256=D8GwmJfuj9Wg3_DeKRPLGGdyHSLcoU2Q0VzKQbkJG4g,2267
4
- fosslight_dependency/_help.py,sha256=KE4u-H2oZETohNZAHh2CY5GoGgUsUSf6CpHNonxgz5A,3671
5
- fosslight_dependency/_package_manager.py,sha256=mN1ukEmZkm6COhxWm-mVfhCZkHppfFgXyXzBT1x02Sw,15016
6
- fosslight_dependency/constant.py,sha256=0Jr0D5T9S3AuLTEgepiqcKvW4nIJeqotz1lt_YDwbGU,1263
4
+ fosslight_dependency/_help.py,sha256=1ER9CDiORhMdX1FYedPHcp2TRyvICPFGM1wxOOJ4PNE,3882
5
+ fosslight_dependency/_package_manager.py,sha256=-hEOG44AnBoSKjMM-WQAbZIXhD8kHc-EwkOl1aLu-CA,15824
6
+ fosslight_dependency/constant.py,sha256=zaLjZsk4r5Gv6puCbV_Crt9pWrX_FEYzgEh0UQVIE4g,1291
7
7
  fosslight_dependency/dependency_item.py,sha256=wNLWcsNycf3HQ5Pib2WrMeo2dn0eHCRg20NLcL95Qew,3345
8
- fosslight_dependency/run_dependency_scanner.py,sha256=4uocLr5HY2r9ouLI0NGLTI8TE-5g3aQlA9eaP7cqBHk,21338
8
+ fosslight_dependency/run_dependency_scanner.py,sha256=9hbrrW_EfJ_6XeajrVFl-B59q5MlMHXsMRJ2BjjKu6o,22736
9
9
  fosslight_dependency/LICENSES/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
10
10
  fosslight_dependency/LICENSES/LicenseRef-3rd_party_licenses.txt,sha256=EcsFt7aE1rp3OXAdJgmXayfOZdpRdBMcmRnyoqWMCsw,95687
11
11
  fosslight_dependency/package_manager/Android.py,sha256=SgQyVxEYvAcCN3EgP9p4QOQZpLIGDahXRm8ntgoCM3c,3666
@@ -15,22 +15,22 @@ fosslight_dependency/package_manager/Cocoapods.py,sha256=k_URV1ekMOU8l_y9_KIp_lu
15
15
  fosslight_dependency/package_manager/Go.py,sha256=eEWvPoE3Jd0lMJAxWMNdFcoi21fJF0EwtRbjBDHF8KQ,7309
16
16
  fosslight_dependency/package_manager/Gradle.py,sha256=zTMvospDpfabl2DRk1jW316lW9BJfGtf05TBcna6E8E,4640
17
17
  fosslight_dependency/package_manager/Helm.py,sha256=ucx2Y0tWX37UHIzIGaRyTe7uQ2vlu2nUuO09hOMq9ZU,4223
18
- fosslight_dependency/package_manager/Maven.py,sha256=h-JkVVT_hNNBM26I677kl6RaLUwAoTB0eGncCo-Qp4k,10980
19
- fosslight_dependency/package_manager/Npm.py,sha256=cPKo3TewAPN8IQUUFlJZ7nc202_GnpMzGMl4gIF4St8,10982
20
- fosslight_dependency/package_manager/Nuget.py,sha256=u4w084Qozk4nrVdT4o_nDiT8v4URIlXaOrDh11Hu1Bw,8885
18
+ fosslight_dependency/package_manager/Maven.py,sha256=F1KQxR_LjSxl7ZgRS_W1TbuDpRERLoAVGLIUWOpjLmk,11046
19
+ fosslight_dependency/package_manager/Npm.py,sha256=6DSsRnk8tj8NUTjG4qFfybi9x-GW1I3FRGYbNn9IJpk,12300
20
+ fosslight_dependency/package_manager/Nuget.py,sha256=YxoyQjd9q8gvdrjs2ZzGhiaIleEq1NjtYQ-xcxIwack,18056
21
21
  fosslight_dependency/package_manager/Pnpm.py,sha256=LDKooFGQHui_Q5U7XqSJ8KcCPiLVndXf5oGKTJExh5w,7056
22
22
  fosslight_dependency/package_manager/Pub.py,sha256=g4jqA19vf3jBdlmPLVZiIYMAQHNn3Y2I-_oq3vtpfv0,10372
23
23
  fosslight_dependency/package_manager/Pypi.py,sha256=Ae-mFl9jSgc1XrUdzAuKGuZA4nduSsWaC-u6VVjFNtg,17187
24
24
  fosslight_dependency/package_manager/Swift.py,sha256=8fdbdAXTNlp2NDoSqQXm48JGAg9UhxA91M1-NhHkT40,6752
25
25
  fosslight_dependency/package_manager/Unity.py,sha256=n1006GZ6Qrk8wAdO6wla1Q-JD7Evin7REVj-HDeTARc,5142
26
- fosslight_dependency/package_manager/Yarn.py,sha256=8K2bTImdvPtuDT_Tz1StnBnx6DjoHWV1Wzb0WlhBRhw,9586
26
+ fosslight_dependency/package_manager/Yarn.py,sha256=ZSi7_O5tMmS80Z58YOZxvai84_KyDpP8plo0x80YtVc,10069
27
27
  fosslight_dependency/package_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- fosslight_dependency-4.1.25.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
29
- fosslight_dependency-4.1.25.dist-info/licenses/LICENSES/Apache-2.0.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
30
- fosslight_dependency-4.1.25.dist-info/licenses/LICENSES/LicenseRef-3rd_party_licenses.txt,sha256=EcsFt7aE1rp3OXAdJgmXayfOZdpRdBMcmRnyoqWMCsw,95687
31
- fosslight_dependency-4.1.25.dist-info/licenses/LICENSES/MIT.txt,sha256=9cx4CbArgByWvkoEZNqpzbpJgA9TUe2D62rMocQpgfs,1082
32
- fosslight_dependency-4.1.25.dist-info/METADATA,sha256=y76t53xjKXM9g869hEAA-t5Rq5jRDrPjy8lLqNFp5sI,5555
33
- fosslight_dependency-4.1.25.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
34
- fosslight_dependency-4.1.25.dist-info/entry_points.txt,sha256=AeU-9Bl8al8Sa-XvhitGHdT3ZTPIrlhqADcp7s5OLF8,90
35
- fosslight_dependency-4.1.25.dist-info/top_level.txt,sha256=Jc0V7VcVCH0TEM8ksb8dwroTYz4AmRaQnlr3FB71Hcs,21
36
- fosslight_dependency-4.1.25.dist-info/RECORD,,
28
+ fosslight_dependency-4.1.26.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
29
+ fosslight_dependency-4.1.26.dist-info/licenses/LICENSES/Apache-2.0.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
30
+ fosslight_dependency-4.1.26.dist-info/licenses/LICENSES/LicenseRef-3rd_party_licenses.txt,sha256=EcsFt7aE1rp3OXAdJgmXayfOZdpRdBMcmRnyoqWMCsw,95687
31
+ fosslight_dependency-4.1.26.dist-info/licenses/LICENSES/MIT.txt,sha256=9cx4CbArgByWvkoEZNqpzbpJgA9TUe2D62rMocQpgfs,1082
32
+ fosslight_dependency-4.1.26.dist-info/METADATA,sha256=PxBOENWXec2lUrHB38ZAwalQB7RXl-OrnA22Z8o9dxg,5555
33
+ fosslight_dependency-4.1.26.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
34
+ fosslight_dependency-4.1.26.dist-info/entry_points.txt,sha256=AeU-9Bl8al8Sa-XvhitGHdT3ZTPIrlhqADcp7s5OLF8,90
35
+ fosslight_dependency-4.1.26.dist-info/top_level.txt,sha256=Jc0V7VcVCH0TEM8ksb8dwroTYz4AmRaQnlr3FB71Hcs,21
36
+ fosslight_dependency-4.1.26.dist-info/RECORD,,