scanoss 1.27.1__py3-none-any.whl → 1.43.1__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.
Files changed (79) hide show
  1. protoc_gen_swagger/options/annotations_pb2.py +18 -12
  2. protoc_gen_swagger/options/annotations_pb2.pyi +48 -0
  3. protoc_gen_swagger/options/annotations_pb2_grpc.py +20 -0
  4. protoc_gen_swagger/options/openapiv2_pb2.py +110 -99
  5. protoc_gen_swagger/options/openapiv2_pb2.pyi +1317 -0
  6. protoc_gen_swagger/options/openapiv2_pb2_grpc.py +20 -0
  7. scanoss/__init__.py +1 -1
  8. scanoss/api/common/v2/scanoss_common_pb2.py +49 -22
  9. scanoss/api/common/v2/scanoss_common_pb2_grpc.py +25 -0
  10. scanoss/api/components/v2/scanoss_components_pb2.py +68 -43
  11. scanoss/api/components/v2/scanoss_components_pb2_grpc.py +83 -22
  12. scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py +136 -47
  13. scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py +650 -33
  14. scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +56 -37
  15. scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +64 -12
  16. scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2.py +74 -31
  17. scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py +252 -13
  18. scanoss/api/licenses/__init__.py +23 -0
  19. scanoss/api/licenses/v2/__init__.py +23 -0
  20. scanoss/api/licenses/v2/scanoss_licenses_pb2.py +84 -0
  21. scanoss/api/licenses/v2/scanoss_licenses_pb2_grpc.py +302 -0
  22. scanoss/api/scanning/v2/scanoss_scanning_pb2.py +32 -21
  23. scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py +49 -8
  24. scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py +50 -23
  25. scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py +151 -16
  26. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py +78 -31
  27. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py +282 -18
  28. scanoss/cli.py +1000 -186
  29. scanoss/components.py +80 -50
  30. scanoss/constants.py +7 -1
  31. scanoss/cryptography.py +89 -55
  32. scanoss/csvoutput.py +13 -7
  33. scanoss/cyclonedx.py +141 -9
  34. scanoss/data/build_date.txt +1 -1
  35. scanoss/data/osadl-copyleft.json +133 -0
  36. scanoss/delta.py +197 -0
  37. scanoss/export/__init__.py +23 -0
  38. scanoss/export/dependency_track.py +227 -0
  39. scanoss/file_filters.py +2 -163
  40. scanoss/filecount.py +37 -38
  41. scanoss/gitlabqualityreport.py +214 -0
  42. scanoss/header_filter.py +563 -0
  43. scanoss/inspection/policy_check/__init__.py +0 -0
  44. scanoss/inspection/policy_check/dependency_track/__init__.py +0 -0
  45. scanoss/inspection/policy_check/dependency_track/project_violation.py +479 -0
  46. scanoss/inspection/{policy_check.py → policy_check/policy_check.py} +65 -72
  47. scanoss/inspection/policy_check/scanoss/__init__.py +0 -0
  48. scanoss/inspection/{copyleft.py → policy_check/scanoss/copyleft.py} +89 -73
  49. scanoss/inspection/{undeclared_component.py → policy_check/scanoss/undeclared_component.py} +52 -46
  50. scanoss/inspection/summary/__init__.py +0 -0
  51. scanoss/inspection/summary/component_summary.py +170 -0
  52. scanoss/inspection/{license_summary.py → summary/license_summary.py} +62 -12
  53. scanoss/inspection/summary/match_summary.py +341 -0
  54. scanoss/inspection/utils/file_utils.py +44 -0
  55. scanoss/inspection/utils/license_utils.py +57 -71
  56. scanoss/inspection/utils/markdown_utils.py +63 -0
  57. scanoss/inspection/{inspect_base.py → utils/scan_result_processor.py} +53 -67
  58. scanoss/osadl.py +125 -0
  59. scanoss/scanner.py +135 -253
  60. scanoss/scanners/folder_hasher.py +47 -32
  61. scanoss/scanners/scanner_hfh.py +50 -18
  62. scanoss/scanoss_settings.py +33 -3
  63. scanoss/scanossapi.py +23 -25
  64. scanoss/scanossbase.py +1 -1
  65. scanoss/scanossgrpc.py +543 -289
  66. scanoss/services/dependency_track_service.py +132 -0
  67. scanoss/spdxlite.py +11 -4
  68. scanoss/threadeddependencies.py +19 -18
  69. scanoss/threadedscanning.py +10 -0
  70. scanoss/utils/scanoss_scan_results_utils.py +41 -0
  71. scanoss/winnowing.py +71 -19
  72. {scanoss-1.27.1.dist-info → scanoss-1.43.1.dist-info}/METADATA +8 -5
  73. scanoss-1.43.1.dist-info/RECORD +110 -0
  74. scanoss/inspection/component_summary.py +0 -94
  75. scanoss-1.27.1.dist-info/RECORD +0 -87
  76. {scanoss-1.27.1.dist-info → scanoss-1.43.1.dist-info}/WHEEL +0 -0
  77. {scanoss-1.27.1.dist-info → scanoss-1.43.1.dist-info}/entry_points.txt +0 -0
  78. {scanoss-1.27.1.dist-info → scanoss-1.43.1.dist-info}/licenses/LICENSE +0 -0
  79. {scanoss-1.27.1.dist-info → scanoss-1.43.1.dist-info}/top_level.txt +0 -0
scanoss/components.py CHANGED
@@ -29,6 +29,9 @@ from typing import List, Optional, TextIO
29
29
 
30
30
  from pypac.parser import PACFile
31
31
 
32
+ from scanoss.cyclonedx import CycloneDx
33
+ from scanoss.utils.file import validate_json_file
34
+
32
35
  from .scanner import Scanner
33
36
  from .scanossbase import ScanossBase
34
37
  from .scanossgrpc import ScanossGrpc
@@ -52,6 +55,8 @@ class Components(ScanossBase):
52
55
  ca_cert: str = None,
53
56
  pac: PACFile = None,
54
57
  req_headers: dict = None,
58
+ ignore_cert_errors: bool = False,
59
+ use_grpc: bool = False,
55
60
  ):
56
61
  """
57
62
  Handle all component style requests
@@ -66,9 +71,13 @@ class Components(ScanossBase):
66
71
  :param grpc_proxy: Specific gRPC proxy (optional)
67
72
  :param ca_cert: TLS client certificate (optional)
68
73
  :param pac: Proxy Auto-Config file (optional)
74
+ :param req_headers: Additional headers to send with requests (optional)
75
+ :param ignore_cert_errors: Ignore TLS certificate errors (optional)
76
+ :param use_grpc: Use gRPC instead of HTTP (optional)
69
77
  """
70
78
  super().__init__(debug, trace, quiet)
71
79
  ver_details = Scanner.version_details()
80
+ self.use_grpc = use_grpc
72
81
  self.grpc_api = ScanossGrpc(
73
82
  url=grpc_url,
74
83
  debug=debug,
@@ -82,26 +91,41 @@ class Components(ScanossBase):
82
91
  grpc_proxy=grpc_proxy,
83
92
  timeout=timeout,
84
93
  req_headers=req_headers,
94
+ ignore_cert_errors=ignore_cert_errors,
85
95
  )
96
+ self.cdx = CycloneDx(debug=self.debug)
97
+
98
+ def load_comps(self, json_file: Optional[str] = None, purls: Optional[List[str]] = None) -> Optional[dict]:
99
+ """
100
+ Load the specified components and return a dictionary
101
+
102
+ :param json_file: JSON Components file (optional)
103
+ :param purls: list pf PURLs (optional)
104
+ :return: Components Request dictionary or None
105
+ """
106
+ return self.load_purls(json_file, purls, 'components')
86
107
 
87
- def load_purls(self, json_file: Optional[str] = None, purls: Optional[List[str]] = None) -> Optional[dict]:
108
+ def load_purls(
109
+ self, json_file: Optional[str] = None, purls: Optional[List[str]] = None, field: str = 'purls'
110
+ ) -> Optional[dict]:
88
111
  """
89
112
  Load the specified purls and return a dictionary
90
113
 
91
114
  :param json_file: JSON PURL file (optional)
92
115
  :param purls: list of PURLs (optional)
116
+ :param field: Name of the dictionary field to store the purls in (default: 'purls')
93
117
  :return: PURL Request dictionary or None
94
118
  """
95
119
  if json_file:
96
- if not os.path.isfile(json_file) or not os.access(json_file, os.R_OK):
97
- self.print_stderr(f'ERROR: JSON file does not exist, is not a file, or is not readable: {json_file}')
120
+ result = validate_json_file(json_file)
121
+ if not result.is_valid:
122
+ self.print_stderr(f'ERROR: Problem parsing input JSON: {result.error}')
98
123
  return None
99
- with open(json_file, 'r') as f:
100
- try:
101
- purl_request = json.loads(f.read())
102
- except Exception as e:
103
- self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
104
- return None
124
+
125
+ if self.cdx.is_cyclonedx_json(json.dumps(result.data)):
126
+ purl_request = self.cdx.get_purls_request_from_cdx(result.data, field)
127
+ else:
128
+ purl_request = result.data
105
129
  elif purls:
106
130
  if not all(isinstance(purl, str) for purl in purls):
107
131
  self.print_stderr('ERROR: PURLs must be a list of strings.')
@@ -109,14 +133,14 @@ class Components(ScanossBase):
109
133
  parsed_purls = []
110
134
  for p in purls:
111
135
  parsed_purls.append({'purl': p})
112
- purl_request = {'purls': parsed_purls}
136
+ purl_request = {field: parsed_purls}
113
137
  else:
114
138
  self.print_stderr('ERROR: No purls specified to process.')
115
139
  return None
116
- purl_count = len(purl_request.get('purls', []))
117
- self.print_debug(f'Parsed Purls ({purl_count}): {purl_request}')
140
+ purl_count = len(purl_request.get(field, []))
141
+ self.print_debug(f'Parsed {field} ({purl_count}): {purl_request}')
118
142
  if purl_count == 0:
119
- self.print_stderr('ERROR: No PURLs parsed from request.')
143
+ self.print_stderr(f'ERROR: No {field} parsed from request.')
120
144
  return None
121
145
  return purl_request
122
146
 
@@ -142,8 +166,8 @@ class Components(ScanossBase):
142
166
  """
143
167
  Open the given filename if requested, otherwise return STDOUT
144
168
 
145
- :param filename:
146
- :return:
169
+ :param filename: filename to open or None to return STDOUT
170
+ :return: file descriptor or None
147
171
  """
148
172
  file = sys.stdout
149
173
  if filename:
@@ -166,32 +190,6 @@ class Components(ScanossBase):
166
190
  self.print_trace(f'Closing file: {filename}')
167
191
  file.close()
168
192
 
169
- def get_crypto_details(self, json_file: str = None, purls: [] = None, output_file: str = None) -> bool:
170
- """
171
- Retrieve the cryptographic details for the supplied PURLs
172
-
173
- :param json_file: PURL JSON request file (optional)
174
- :param purls: PURL request array (optional)
175
- :param output_file: output filename (optional). Default: STDOUT
176
- :return: True on success, False otherwise
177
- """
178
- success = False
179
- purls_request = self.load_purls(json_file, purls)
180
- if purls_request is None or len(purls_request) == 0:
181
- return False
182
- file = self._open_file_or_sdtout(output_file)
183
- if file is None:
184
- return False
185
- self.print_msg('Sending PURLs to Crypto API for decoration...')
186
- response = self.grpc_api.get_crypto_json(purls_request)
187
- if response:
188
- print(json.dumps(response, indent=2, sort_keys=True), file=file)
189
- success = True
190
- if output_file:
191
- self.print_msg(f'Results written to: {output_file}')
192
- self._close_file(output_file, file)
193
- return success
194
-
195
193
  def get_vulnerabilities(self, json_file: str = None, purls: [] = None, output_file: str = None) -> bool:
196
194
  """
197
195
  Retrieve any vulnerabilities related to the given PURLs
@@ -202,14 +200,14 @@ class Components(ScanossBase):
202
200
  :return: True on success, False otherwise
203
201
  """
204
202
  success = False
205
- purls_request = self.load_purls(json_file, purls)
203
+ purls_request = self.load_comps(json_file, purls)
206
204
  if purls_request is None or len(purls_request) == 0:
207
205
  return False
208
206
  file = self._open_file_or_sdtout(output_file)
209
207
  if file is None:
210
208
  return False
211
209
  self.print_msg('Sending PURLs to Vulnerability API for decoration...')
212
- response = self.grpc_api.get_vulnerabilities_json(purls_request)
210
+ response = self.grpc_api.get_vulnerabilities_json(purls_request, use_grpc=self.use_grpc)
213
211
  if response:
214
212
  print(json.dumps(response, indent=2, sort_keys=True), file=file)
215
213
  success = True
@@ -228,14 +226,14 @@ class Components(ScanossBase):
228
226
  :return: True on success, False otherwise
229
227
  """
230
228
  success = False
231
- purls_request = self.load_purls(json_file, purls)
229
+ purls_request = self.load_comps(json_file, purls)
232
230
  if purls_request is None or len(purls_request) == 0:
233
231
  return False
234
232
  file = self._open_file_or_sdtout(output_file)
235
233
  if file is None:
236
234
  return False
237
235
  self.print_msg('Sending PURLs to Semgrep API for decoration...')
238
- response = self.grpc_api.get_semgrep_json(purls_request)
236
+ response = self.grpc_api.get_semgrep_json(purls_request, use_grpc=self.use_grpc)
239
237
  if response:
240
238
  print(json.dumps(response, indent=2, sort_keys=True), file=file)
241
239
  success = True
@@ -285,7 +283,7 @@ class Components(ScanossBase):
285
283
  if file is None:
286
284
  return False
287
285
  self.print_msg('Sending search data to Components API...')
288
- response = self.grpc_api.search_components_json(request)
286
+ response = self.grpc_api.search_components_json(request, use_grpc=self.use_grpc)
289
287
  if response:
290
288
  print(json.dumps(response, indent=2, sort_keys=True), file=file)
291
289
  success = True
@@ -321,7 +319,7 @@ class Components(ScanossBase):
321
319
  if file is None:
322
320
  return False
323
321
  self.print_msg('Sending PURLs to Component Versions API...')
324
- response = self.grpc_api.get_component_versions_json(request)
322
+ response = self.grpc_api.get_component_versions_json(request, use_grpc=self.use_grpc)
325
323
  if response:
326
324
  print(json.dumps(response, indent=2, sort_keys=True), file=file)
327
325
  success = True
@@ -346,7 +344,7 @@ class Components(ScanossBase):
346
344
  bool: True on success, False otherwise
347
345
  """
348
346
  success = False
349
- purls_request = self.load_purls(json_file, purls)
347
+ purls_request = self.load_comps(json_file, purls)
350
348
  if purls_request is None or len(purls_request) == 0:
351
349
  return False
352
350
  file = self._open_file_or_sdtout(output_file)
@@ -354,10 +352,42 @@ class Components(ScanossBase):
354
352
  return False
355
353
  if origin:
356
354
  self.print_msg('Sending PURLs to Geo Provenance Origin API for decoration...')
357
- response = self.grpc_api.get_provenance_origin(purls_request)
355
+ response = self.grpc_api.get_provenance_origin(purls_request, use_grpc=self.use_grpc)
358
356
  else:
359
357
  self.print_msg('Sending PURLs to Geo Provenance Declared API for decoration...')
360
- response = self.grpc_api.get_provenance_json(purls_request)
358
+ response = self.grpc_api.get_provenance_json(purls_request, use_grpc=self.use_grpc)
359
+ if response:
360
+ print(json.dumps(response, indent=2, sort_keys=True), file=file)
361
+ success = True
362
+ if output_file:
363
+ self.print_msg(f'Results written to: {output_file}')
364
+ self._close_file(output_file, file)
365
+ return success
366
+
367
+ def get_licenses(self, json_file: str = None, purls: [] = None, output_file: str = None) -> bool:
368
+ """
369
+ Retrieve the license details for the supplied PURLs
370
+
371
+ Args:
372
+ json_file (str, optional): Input JSON file. Defaults to None.
373
+ purls (None, optional): PURLs to retrieve license details for. Defaults to None.
374
+ output_file (str, optional): Output file. Defaults to None.
375
+
376
+ Returns:
377
+ bool: True on success, False otherwise
378
+ """
379
+ success = False
380
+
381
+ purls_request = self.load_purls(json_file, purls)
382
+ if not purls_request:
383
+ return False
384
+ file = self._open_file_or_sdtout(output_file)
385
+ if file is None:
386
+ return False
387
+
388
+ # We'll use the new ComponentBatchRequest instead of deprecated PurlRequest for the license api
389
+ component_batch_request = {'components': purls_request.get('purls')}
390
+ response = self.grpc_api.get_licenses(component_batch_request, use_grpc=self.use_grpc)
361
391
  if response:
362
392
  print(json.dumps(response, indent=2, sort_keys=True), file=file)
363
393
  success = True
scanoss/constants.py CHANGED
@@ -13,4 +13,10 @@ DEFAULT_URL2 = 'https://api.scanoss.com' # default premium service URL
13
13
 
14
14
  DEFAULT_API_TIMEOUT = 600
15
15
 
16
- DEFAULT_HFH_RANK_THRESHOLD = 5
16
+ DEFAULT_HFH_RANK_THRESHOLD = 5
17
+ DEFAULT_HFH_DEPTH = 1
18
+ DEFAULT_HFH_RECURSIVE_THRESHOLD = 0.8
19
+ DEFAULT_HFH_MIN_ACCEPTED_SCORE = 0.15
20
+
21
+ VALID_LICENSE_SOURCES = ['component_declared', 'license_file', 'file_header', 'file_spdx_tag', 'scancode']
22
+ DEFAULT_COPYLEFT_LICENSE_SOURCES = ['component_declared', 'license_file']
scanoss/cryptography.py CHANGED
@@ -2,6 +2,7 @@ import json
2
2
  from dataclasses import dataclass
3
3
  from typing import Dict, List, Optional
4
4
 
5
+ from scanoss.cyclonedx import CycloneDx
5
6
  from scanoss.scanossbase import ScanossBase
6
7
  from scanoss.scanossgrpc import ScanossGrpc
7
8
  from scanoss.utils.abstract_presenter import AbstractPresenter
@@ -18,14 +19,47 @@ MIN_SPLIT_PARTS = 2
18
19
  @dataclass
19
20
  class CryptographyConfig:
20
21
  purl: List[str]
22
+ debug: bool = False
23
+ header: Optional[str] = None
21
24
  input_file: Optional[str] = None
22
25
  output_file: Optional[str] = None
23
- header: Optional[str] = None
24
- debug: bool = False
25
- trace: bool = False
26
26
  quiet: bool = False
27
+ trace: bool = False
28
+ use_grpc: bool = False
27
29
  with_range: bool = False
28
30
 
31
+ def _process_input_file(self) -> dict:
32
+ """
33
+ Process and validate the input file, returning the validated purl_request.
34
+
35
+ Returns:
36
+ dict: The validated purl_request dictionary
37
+
38
+ Raises:
39
+ ScanossCryptographyError: If the input file is invalid
40
+ """
41
+ result = validate_json_file(self.input_file)
42
+ if not result.is_valid:
43
+ raise ScanossCryptographyError(
44
+ f'There was a problem with the purl input file. {result.error}'
45
+ )
46
+
47
+ cdx = CycloneDx(debug=self.debug)
48
+ if cdx.is_cyclonedx_json(json.dumps(result.data)):
49
+ purl_request = cdx.get_purls_request_from_cdx(result.data)
50
+ else:
51
+ purl_request = result.data
52
+
53
+ if (
54
+ not isinstance(purl_request, dict)
55
+ or 'purls' not in purl_request
56
+ or not isinstance(purl_request['purls'], list)
57
+ or not all(isinstance(p, dict) and 'purl' in p for p in purl_request['purls'])
58
+ ):
59
+ raise ScanossCryptographyError('The supplied input file is not in the correct PurlRequest format.')
60
+
61
+ return purl_request
62
+
29
63
  def __post_init__(self):
30
64
  """
31
65
  Validate that the configuration is valid.
@@ -36,46 +70,36 @@ class CryptographyConfig:
36
70
  parts = purl.split('@')
37
71
  if not (len(parts) >= MIN_SPLIT_PARTS and parts[1]):
38
72
  raise ScanossCryptographyError(
39
- f'Invalid PURL format: "{purl}".' f'It must include a version (e.g., pkg:type/name@version)'
73
+ f'Invalid PURL format: "{purl}".It must include a version (e.g., pkg:type/name@version)'
40
74
  )
41
75
  if self.input_file:
42
- input_file_validation = validate_json_file(self.input_file)
43
- if not input_file_validation.is_valid:
76
+ purl_request = self._process_input_file()
77
+ purls = purl_request['purls']
78
+ purls_with_requirement = []
79
+ if self.with_range and any('requirement' not in p for p in purls):
44
80
  raise ScanossCryptographyError(
45
- f'There was a problem with the purl input file. {input_file_validation.error}'
81
+ f'One or more PURLs in "{self.input_file}" are missing the "requirement" field.'
46
82
  )
47
- if (
48
- not isinstance(input_file_validation.data, dict)
49
- or 'purls' not in input_file_validation.data
50
- or not isinstance(input_file_validation.data['purls'], list)
51
- or not all(isinstance(p, dict) and 'purl' in p for p in input_file_validation.data['purls'])
52
- ):
53
- raise ScanossCryptographyError('The supplied input file is not in the correct PurlRequest format.')
54
- purls = input_file_validation.data['purls']
55
- purls_with_requirement = []
56
- if self.with_range:
57
- if any('requirement' not in p for p in purls):
58
- raise ScanossCryptographyError(
59
- f'One or more PURLs in "{self.input_file}" are missing the "requirement" field.'
60
- )
83
+
84
+ for purl in purls:
85
+ if 'requirement' in purl:
86
+ purls_with_requirement.append(f'{purl["purl"]}@{purl["requirement"]}')
61
87
  else:
62
- for purl in purls:
63
- purls_with_requirement.append(f'{purl["purl"]}@{purl["requirement"]}')
64
- else:
65
- purls_with_requirement = purls
88
+ purls_with_requirement.append(purl['purl'])
66
89
  self.purl = purls_with_requirement
67
90
 
68
91
 
69
92
  def create_cryptography_config_from_args(args) -> CryptographyConfig:
70
93
  return CryptographyConfig(
71
94
  debug=getattr(args, 'debug', False),
72
- trace=getattr(args, 'trace', False),
73
- quiet=getattr(args, 'quiet', False),
74
- with_range=getattr(args, 'with_range', False),
75
- purl=getattr(args, 'purl', []),
95
+ header=getattr(args, 'header', None),
76
96
  input_file=getattr(args, 'input', None),
77
97
  output_file=getattr(args, 'output', None),
78
- header=getattr(args, 'header', None),
98
+ purl=getattr(args, 'purl', []),
99
+ quiet=getattr(args, 'quiet', False),
100
+ trace=getattr(args, 'trace', False),
101
+ use_grpc=getattr(args, 'grpc', False),
102
+ with_range=getattr(args, 'with_range', False),
79
103
  )
80
104
 
81
105
 
@@ -112,7 +136,7 @@ class Cryptography:
112
136
 
113
137
  self.client = client
114
138
  self.config = config
115
- self.purls_request = self._build_purls_request()
139
+ self.components_request = self._build_components_request()
116
140
  self.results = None
117
141
 
118
142
  def get_algorithms(self) -> Optional[Dict]:
@@ -123,15 +147,16 @@ class Cryptography:
123
147
  Optional[Dict]: The folder hash response from the gRPC client, or None if an error occurs.
124
148
  """
125
149
 
126
- if not self.purls_request:
150
+ if not self.components_request or not self.components_request.get('components'):
127
151
  raise ScanossCryptographyError('No PURLs supplied. Provide --purl or --input.')
128
- self.base.print_stderr(
129
- f'Getting cryptographic algorithms for {", ".join([p["purl"] for p in self.purls_request["purls"]])}'
130
- )
152
+ components_str = ', '.join(p['purl'] for p in self.components_request['components'])
153
+ self.base.print_stderr(f'Getting cryptographic algorithms for {components_str}')
131
154
  if self.config.with_range:
132
- response = self.client.get_crypto_algorithms_in_range_for_purl(self.purls_request)
155
+ response = self.client.get_crypto_algorithms_in_range_for_purl(
156
+ self.components_request, self.config.use_grpc
157
+ )
133
158
  else:
134
- response = self.client.get_crypto_algorithms_for_purl(self.purls_request)
159
+ response = self.client.get_crypto_algorithms_for_purl(self.components_request, self.config.use_grpc)
135
160
  if response:
136
161
  self.results = response
137
162
 
@@ -145,17 +170,17 @@ class Cryptography:
145
170
  Optional[Dict]: The encryption hints response from the gRPC client, or None if an error occurs.
146
171
  """
147
172
 
148
- if not self.purls_request:
173
+ if not self.components_request or not self.components_request.get('components'):
149
174
  raise ScanossCryptographyError('No PURLs supplied. Provide --purl or --input.')
150
175
  self.base.print_stderr(
151
176
  f'Getting encryption hints '
152
177
  f'{"in range" if self.config.with_range else ""} '
153
- f'for {", ".join([p["purl"] for p in self.purls_request["purls"]])}'
178
+ f'for {", ".join([p["purl"] for p in self.components_request["components"]])}'
154
179
  )
155
180
  if self.config.with_range:
156
- response = self.client.get_encryption_hints_in_range_for_purl(self.purls_request)
181
+ response = self.client.get_encryption_hints_in_range_for_purl(self.components_request, self.config.use_grpc)
157
182
  else:
158
- response = self.client.get_encryption_hints_for_purl(self.purls_request)
183
+ response = self.client.get_encryption_hints_for_purl(self.components_request, self.config.use_grpc)
159
184
  if response:
160
185
  self.results = response
161
186
 
@@ -169,42 +194,51 @@ class Cryptography:
169
194
  Optional[Dict]: The versions in range response from the gRPC client, or None if an error occurs.
170
195
  """
171
196
 
172
- if not self.purls_request:
197
+ if not self.components_request or not self.components_request.get('components'):
173
198
  raise ScanossCryptographyError('No PURLs supplied. Provide --purl or --input.')
174
199
 
175
- self.base.print_stderr(
176
- f'Getting versions in range for {", ".join([p["purl"] for p in self.purls_request["purls"]])}'
177
- )
200
+ components_str = ', '.join(p['purl'] for p in self.components_request['components'])
201
+ self.base.print_stderr(f'Getting versions in range for {components_str}')
178
202
 
179
- response = self.client.get_versions_in_range_for_purl(self.purls_request)
203
+ response = self.client.get_versions_in_range_for_purl(self.components_request, self.config.use_grpc)
180
204
  if response:
181
205
  self.results = response
182
206
 
183
207
  return self.results
184
208
 
185
- def _build_purls_request(
209
+ def _build_components_request(
186
210
  self,
187
211
  ) -> Optional[dict]:
188
212
  """
189
213
  Load the specified purls from a JSON file or a list of PURLs and return a dictionary
190
214
 
191
- Args:
192
- json_file (Optional[str], optional): The JSON file containing the PURLs. Defaults to None.
193
- purls (Optional[List[str]], optional): The list of PURLs. Defaults to None.
194
-
195
215
  Returns:
196
216
  Optional[dict]: The dictionary containing the PURLs
197
217
  """
198
218
  return {
199
- 'purls': [
219
+ 'components': [
200
220
  {
201
- 'purl': p,
202
- 'requirement': self._extract_version_from_purl(p),
221
+ 'purl': self._remove_version_from_purl(purl),
222
+ 'requirement': self._extract_version_from_purl(purl),
203
223
  }
204
- for p in self.config.purl
224
+ for purl in self.config.purl
205
225
  ]
206
226
  }
207
227
 
228
+ def _remove_version_from_purl(self, purl: str) -> str:
229
+ """
230
+ Remove version from purl
231
+
232
+ Args:
233
+ purl (str): The purl string to remove the version from
234
+
235
+ Returns:
236
+ str: The purl string without the version
237
+ """
238
+ if '@' not in purl:
239
+ return purl
240
+ return purl.split('@')[0]
241
+
208
242
  def _extract_version_from_purl(self, purl: str) -> str:
209
243
  """
210
244
  Extract version from purl
scanoss/csvoutput.py CHANGED
@@ -21,11 +21,10 @@ SPDX-License-Identifier: MIT
21
21
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
22
  THE SOFTWARE.
23
23
  """
24
-
24
+ import csv
25
25
  import json
26
26
  import os.path
27
27
  import sys
28
- import csv
29
28
 
30
29
  from .scanossbase import ScanossBase
31
30
 
@@ -44,16 +43,20 @@ class CsvOutput(ScanossBase):
44
43
  self.output_file = output_file
45
44
  self.debug = debug
46
45
 
47
- def parse(self, data: json):
46
+ # TODO Refactor (fails linter)
47
+ def parse(self, data: json): #noqa PLR0912, PLR0915
48
48
  """
49
49
  Parse the given input (raw/plain) JSON string and return CSV summary
50
50
  :param data: json - JSON object
51
51
  :return: CSV dictionary
52
52
  """
53
- if not data:
53
+ if data is None:
54
54
  self.print_stderr('ERROR: No JSON data provided to parse.')
55
55
  return None
56
- self.print_debug(f'Processing raw results into CSV format...')
56
+ if len(data) == 0:
57
+ self.print_msg('Warning: Empty scan results provided. Returning empty CSV list.')
58
+ return []
59
+ self.print_debug('Processing raw results into CSV format...')
57
60
  csv_dict = []
58
61
  row_id = 1
59
62
  for f in data:
@@ -92,7 +95,8 @@ class CsvOutput(ScanossBase):
92
95
  detected['licenses'] = ''
93
96
  else:
94
97
  detected['licenses'] = ';'.join(dc)
95
- # inventory_id,path,usage,detected_component,detected_license,detected_version,detected_latest,purl
98
+ # inventory_id,path,usage,detected_component,detected_license,
99
+ # detected_version,detected_latest,purl
96
100
  csv_dict.append(
97
101
  {
98
102
  'inventory_id': row_id,
@@ -183,9 +187,11 @@ class CsvOutput(ScanossBase):
183
187
  :return: True if successful, False otherwise
184
188
  """
185
189
  csv_data = self.parse(data)
186
- if not csv_data:
190
+ if csv_data is None:
187
191
  self.print_stderr('ERROR: No CSV data returned for the JSON string provided.')
188
192
  return False
193
+ if len(csv_data) == 0:
194
+ self.print_msg('Warning: Empty scan results - generating CSV with headers only.')
189
195
  # Header row/column details
190
196
  fields = [
191
197
  'inventory_id',