scanoss 1.32.0__py3-none-any.whl → 1.34.0__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 (34) 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 +8 -6
  9. scanoss/api/common/v2/scanoss_common_pb2_grpc.py +5 -1
  10. scanoss/api/components/v2/scanoss_components_pb2.py +46 -32
  11. scanoss/api/components/v2/scanoss_components_pb2_grpc.py +6 -6
  12. scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py +107 -29
  13. scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py +545 -9
  14. scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +29 -21
  15. scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +1 -0
  16. scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2.py +51 -19
  17. scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py +189 -1
  18. scanoss/api/licenses/v2/scanoss_licenses_pb2.py +27 -27
  19. scanoss/api/scanning/v2/scanoss_scanning_pb2.py +18 -18
  20. scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py +29 -13
  21. scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py +102 -8
  22. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py +21 -21
  23. scanoss/cli.py +193 -146
  24. scanoss/components.py +57 -46
  25. scanoss/cryptography.py +64 -44
  26. scanoss/cyclonedx.py +22 -0
  27. scanoss/data/build_date.txt +1 -1
  28. scanoss/scanossgrpc.py +433 -314
  29. {scanoss-1.32.0.dist-info → scanoss-1.34.0.dist-info}/METADATA +4 -3
  30. {scanoss-1.32.0.dist-info → scanoss-1.34.0.dist-info}/RECORD +34 -32
  31. {scanoss-1.32.0.dist-info → scanoss-1.34.0.dist-info}/WHEEL +0 -0
  32. {scanoss-1.32.0.dist-info → scanoss-1.34.0.dist-info}/entry_points.txt +0 -0
  33. {scanoss-1.32.0.dist-info → scanoss-1.34.0.dist-info}/licenses/LICENSE +0 -0
  34. {scanoss-1.32.0.dist-info → scanoss-1.34.0.dist-info}/top_level.txt +0 -0
scanoss/scanossgrpc.py CHANGED
@@ -24,14 +24,13 @@ SPDX-License-Identifier: MIT
24
24
 
25
25
  import concurrent.futures
26
26
  import http.client as http_client
27
- import json
28
27
  import logging
29
28
  import os
30
29
  import sys
31
30
  import time
32
31
  import uuid
33
32
  from dataclasses import dataclass
34
- from enum import IntEnum
33
+ from enum import Enum, IntEnum
35
34
  from typing import Dict, Optional
36
35
  from urllib.parse import urlparse
37
36
 
@@ -44,6 +43,7 @@ from pypac.parser import PACFile
44
43
  from pypac.resolver import ProxyResolver
45
44
  from urllib3.exceptions import InsecureRequestWarning
46
45
 
46
+ from scanoss.api.licenses.v2.scanoss_licenses_pb2_grpc import LicenseStub
47
47
  from scanoss.api.scanning.v2.scanoss_scanning_pb2_grpc import ScanningStub
48
48
  from scanoss.constants import DEFAULT_TIMEOUT
49
49
 
@@ -51,27 +51,20 @@ from . import __version__
51
51
  from .api.common.v2.scanoss_common_pb2 import (
52
52
  ComponentsRequest,
53
53
  EchoRequest,
54
- EchoResponse,
55
- PurlRequest,
56
54
  StatusCode,
57
55
  StatusResponse,
58
56
  )
59
57
  from .api.components.v2.scanoss_components_pb2 import (
60
58
  CompSearchRequest,
61
- CompSearchResponse,
62
59
  CompVersionRequest,
63
- CompVersionResponse,
64
60
  )
65
61
  from .api.components.v2.scanoss_components_pb2_grpc import ComponentsStub
66
62
  from .api.cryptography.v2.scanoss_cryptography_pb2_grpc import CryptographyStub
67
63
  from .api.dependencies.v2.scanoss_dependencies_pb2 import DependencyRequest
68
64
  from .api.dependencies.v2.scanoss_dependencies_pb2_grpc import DependenciesStub
69
- from .api.geoprovenance.v2.scanoss_geoprovenance_pb2 import ContributorResponse
70
65
  from .api.geoprovenance.v2.scanoss_geoprovenance_pb2_grpc import GeoProvenanceStub
71
66
  from .api.scanning.v2.scanoss_scanning_pb2 import HFHRequest
72
- from .api.semgrep.v2.scanoss_semgrep_pb2 import SemgrepResponse
73
67
  from .api.semgrep.v2.scanoss_semgrep_pb2_grpc import SemgrepStub
74
- from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2 import ComponentsVulnerabilityResponse
75
68
  from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2_grpc import VulnerabilitiesStub
76
69
  from .scanossbase import ScanossBase
77
70
 
@@ -81,24 +74,66 @@ SCANOSS_GRPC_URL = os.environ.get('SCANOSS_GRPC_URL') if os.environ.get('SCANOSS
81
74
  SCANOSS_API_KEY = os.environ.get('SCANOSS_API_KEY') if os.environ.get('SCANOSS_API_KEY') else ''
82
75
  DEFAULT_URI_PREFIX = '/v2'
83
76
 
84
- MAX_CONCURRENT_REQUESTS = 5 # Maximum number of concurrent requests to make
77
+ MAX_CONCURRENT_REQUESTS = 5 # Maximum number of concurrent requests to make
78
+
79
+ # REST API endpoint mappings with HTTP methods
80
+ REST_ENDPOINTS = {
81
+ 'vulnerabilities.GetComponentsVulnerabilities': {'path': '/vulnerabilities/components', 'method': 'POST'},
82
+ 'dependencies.Echo': {'path': '/dependencies/echo', 'method': 'POST'},
83
+ 'dependencies.GetDependencies': {'path': '/dependencies/dependencies', 'method': 'POST'},
84
+ 'cryptography.Echo': {'path': '/cryptography/echo', 'method': 'POST'},
85
+ 'cryptography.GetComponentsAlgorithms': {'path': '/cryptography/algorithms/components', 'method': 'POST'},
86
+ 'cryptography.GetComponentsAlgorithmsInRange': {
87
+ 'path': '/cryptography/algorithms/range/components',
88
+ 'method': 'POST',
89
+ },
90
+ 'cryptography.GetComponentsEncryptionHints': {'path': '/cryptography/hints/components', 'method': 'POST'},
91
+ 'cryptography.GetComponentsHintsInRange': {'path': '/cryptography/hints/components/range', 'method': 'POST'},
92
+ 'cryptography.GetComponentsVersionsInRange': {
93
+ 'path': '/cryptography/algorithms/versions/range/components',
94
+ 'method': 'POST',
95
+ },
96
+ 'components.SearchComponents': {'path': '/components/search', 'method': 'GET'},
97
+ 'components.GetComponentVersions': {'path': '/components/versions', 'method': 'GET'},
98
+ 'geoprovenance.GetCountryContributorsByComponents': {
99
+ 'path': '/geoprovenance/countries/components',
100
+ 'method': 'POST',
101
+ },
102
+ 'geoprovenance.GetOriginByComponents': {'path': '/geoprovenance/origin/components', 'method': 'POST'},
103
+ 'licenses.GetComponentsLicenses': {'path': '/licenses/components', 'method': 'POST'},
104
+ 'semgrep.GetComponentsIssues': {'path': '/semgrep/issues/components', 'method': 'POST'},
105
+ 'scanning.FolderHashScan': {'path': '/scanning/hfh/scan', 'method': 'POST'},
106
+ }
85
107
 
86
108
 
87
109
  class ScanossGrpcError(Exception):
88
110
  """
89
111
  Custom exception for SCANOSS gRPC errors
90
112
  """
113
+
91
114
  pass
92
115
 
93
116
 
94
117
  class ScanossGrpcStatusCode(IntEnum):
95
118
  """Status codes for SCANOSS gRPC responses"""
119
+
120
+ UNSPECIFIED = 0
96
121
  SUCCESS = 1
97
- SUCCESS_WITH_WARNINGS = 2
98
- FAILED_WITH_WARNINGS = 3
122
+ SUCCEEDED_WITH_WARNINGS = 2
123
+ WARNING = 3
99
124
  FAILED = 4
100
125
 
101
126
 
127
+ class ScanossRESTStatusCode(Enum):
128
+ """Status codes for SCANOSS REST responses"""
129
+
130
+ UNSPECIFIED = 'UNSPECIFIED'
131
+ SUCCESS = 'SUCCESS'
132
+ SUCCEEDED_WITH_WARNINGS = 'SUCCEEDED_WITH_WARNINGS'
133
+ WARNING = 'WARNING'
134
+ FAILED = 'FAILED'
135
+
136
+
102
137
  class ScanossGrpc(ScanossBase):
103
138
  """
104
139
  Client for gRPC functionality
@@ -106,20 +141,20 @@ class ScanossGrpc(ScanossBase):
106
141
 
107
142
  def __init__( # noqa: PLR0912, PLR0913, PLR0915
108
143
  self,
109
- url: str = None,
144
+ url: Optional[str] = None,
110
145
  debug: bool = False,
111
146
  trace: bool = False,
112
147
  quiet: bool = False,
113
- ca_cert: str = None,
114
- api_key: str = None,
115
- ver_details: str = None,
148
+ ca_cert: Optional[str] = None,
149
+ api_key: Optional[str] = None,
150
+ ver_details: Optional[str] = None,
116
151
  timeout: int = 600,
117
- proxy: str = None,
118
- grpc_proxy: str = None,
119
- pac: PACFile = None,
120
- req_headers: dict = None,
152
+ proxy: Optional[str] = None,
153
+ grpc_proxy: Optional[str] = None,
154
+ pac: Optional[PACFile] = None,
155
+ req_headers: Optional[dict] = None,
121
156
  ignore_cert_errors: bool = False,
122
- use_grpc: bool = False,
157
+ use_grpc: Optional[bool] = False,
123
158
  ):
124
159
  """
125
160
 
@@ -208,6 +243,7 @@ class ScanossGrpc(ScanossBase):
208
243
  self.vuln_stub = VulnerabilitiesStub(grpc.insecure_channel(self.url))
209
244
  self.provenance_stub = GeoProvenanceStub(grpc.insecure_channel(self.url))
210
245
  self.scanning_stub = ScanningStub(grpc.insecure_channel(self.url))
246
+ self.license_stub = LicenseStub(grpc.insecure_channel(self.url))
211
247
  else:
212
248
  if ca_cert is not None:
213
249
  credentials = grpc.ssl_channel_credentials(cert_data) # secure with specified certificate
@@ -220,59 +256,32 @@ class ScanossGrpc(ScanossBase):
220
256
  self.vuln_stub = VulnerabilitiesStub(grpc.secure_channel(self.url, credentials))
221
257
  self.provenance_stub = GeoProvenanceStub(grpc.secure_channel(self.url, credentials))
222
258
  self.scanning_stub = ScanningStub(grpc.secure_channel(self.url, credentials))
259
+ self.license_stub = LicenseStub(grpc.secure_channel(self.url, credentials))
223
260
 
224
261
  @classmethod
225
262
  def _load_cert(cls, cert_file: str) -> bytes:
226
263
  with open(cert_file, 'rb') as f:
227
264
  return f.read()
228
265
 
229
- def deps_echo(self, message: str = 'Hello there!') -> str:
266
+ def deps_echo(self, message: str = 'Hello there!') -> Optional[dict]:
230
267
  """
231
268
  Send Echo message to the Dependency service
232
269
  :param self:
233
270
  :param message: Message to send (default: Hello there!)
234
271
  :return: echo or None
235
272
  """
236
- request_id = str(uuid.uuid4())
237
- resp: EchoResponse
238
- try:
239
- metadata = self.metadata[:]
240
- metadata.append(('x-request-id', request_id)) # Set a Request ID
241
- resp = self.dependencies_stub.Echo(EchoRequest(message=message), metadata=metadata, timeout=3)
242
- except Exception as e:
243
- self.print_stderr(
244
- f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
245
- )
246
- else:
247
- if resp:
248
- return resp.message
249
- self.print_stderr(f'ERROR: Problem sending Echo request ({message}) to {self.url}. rqId: {request_id}')
250
- return None
273
+ return self._call_api('dependencies.Echo', self.dependencies_stub.Echo, {'message': message}, EchoRequest)
251
274
 
252
- def crypto_echo(self, message: str = 'Hello there!') -> str:
275
+ def crypto_echo(self, message: str = 'Hello there!') -> Optional[dict]:
253
276
  """
254
277
  Send Echo message to the Cryptography service
255
278
  :param self:
256
279
  :param message: Message to send (default: Hello there!)
257
280
  :return: echo or None
258
281
  """
259
- request_id = str(uuid.uuid4())
260
- resp: EchoResponse
261
- try:
262
- metadata = self.metadata[:]
263
- metadata.append(('x-request-id', request_id)) # Set a Request ID
264
- resp = self.crypto_stub.Echo(EchoRequest(message=message), metadata=metadata, timeout=3)
265
- except Exception as e:
266
- self.print_stderr(
267
- f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
268
- )
269
- else:
270
- if resp:
271
- return resp.message
272
- self.print_stderr(f'ERROR: Problem sending Echo request ({message}) to {self.url}. rqId: {request_id}')
273
- return None
282
+ return self._call_api('cryptography.Echo', self.crypto_stub.Echo, {'message': message}, EchoRequest)
274
283
 
275
- def get_dependencies(self, dependencies: json, depth: int = 1) -> dict:
284
+ def get_dependencies(self, dependencies: Optional[dict] = None, depth: int = 1) -> Optional[dict]:
276
285
  if not dependencies:
277
286
  self.print_stderr('ERROR: No dependency data supplied to submit to the API.')
278
287
  return None
@@ -281,7 +290,7 @@ class ScanossGrpc(ScanossBase):
281
290
  self.print_stderr(f'ERROR: No response for dependency request: {dependencies}')
282
291
  return resp
283
292
 
284
- def get_dependencies_json(self, dependencies: dict, depth: int = 1) -> dict:
293
+ def get_dependencies_json(self, dependencies: dict, depth: int = 1) -> Optional[dict]:
285
294
  """
286
295
  Client function to call the rpc for GetDependencies
287
296
  :param dependencies: Message to send to the service
@@ -296,11 +305,11 @@ class ScanossGrpc(ScanossBase):
296
305
  self.print_stderr('ERROR: No dependency data supplied to send to decoration service.')
297
306
  return None
298
307
  all_responses = []
299
- # determine if we are using gRPC or REST based on the use_grpc flag
300
- process_file = self._process_dep_file_grpc if self.use_grpc else self._process_dep_file_rest
301
308
  # Process the dependency files in parallel
302
309
  with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_CONCURRENT_REQUESTS) as executor:
303
- future_to_file = {executor.submit(process_file, file, depth): file for file in files_json}
310
+ future_to_file = {
311
+ executor.submit(self._process_dep_file, file, depth, self.use_grpc): file for file in files_json
312
+ }
304
313
  for future in concurrent.futures.as_completed(future_to_file):
305
314
  response = future.result()
306
315
  if response:
@@ -318,161 +327,104 @@ class ScanossGrpc(ScanossBase):
318
327
  merged_response['status'] = response['status']
319
328
  return merged_response
320
329
 
321
- def _process_dep_file_grpc(self, file, depth: int = 1) -> dict:
330
+ def _process_dep_file(self, file, depth: int = 1, use_grpc: Optional[bool] = None) -> Optional[dict]:
322
331
  """
323
- Process a single file using gRPC
332
+ Process a single dependency file using either gRPC or REST
324
333
 
325
- :param file: dependency file purls
326
- :param depth: depth to search (default: 1)
327
- :return: response JSON or None
328
- """
329
- request_id = str(uuid.uuid4())
330
- try:
331
- file_request = {'files': [file]}
332
- request = ParseDict(file_request, DependencyRequest())
333
- request.depth = depth
334
- metadata = self.metadata[:]
335
- metadata.append(('x-request-id', request_id))
336
- self.print_debug(f'Sending dependency data via gRPC for decoration (rqId: {request_id})...')
337
- resp = self.dependencies_stub.GetDependencies(request, metadata=metadata, timeout=self.timeout)
338
- return MessageToDict(resp, preserving_proto_field_name=True)
339
- except Exception as e:
340
- self.print_stderr(
341
- f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
342
- )
343
- return None
334
+ Args:
335
+ file: dependency file purls
336
+ depth: depth to search (default: 1)
337
+ use_grpc: Whether to use gRPC or REST (None = use instance default)
344
338
 
345
- def get_vulnerabilities_json(self, purls: dict) -> dict:
346
- """
347
- Client function to call the rpc for Vulnerability GetVulnerabilities
348
- It will either use REST (default) or gRPC depending on the use_grpc flag
349
- :param purls: Message to send to the service
350
- :return: Server response or None
339
+ Returns:
340
+ response JSON or None
351
341
  """
352
- if self.use_grpc:
353
- return self._get_vulnerabilities_grpc(purls)
354
- else:
355
- return self._get_vulnerabilities_rest(purls)
342
+ file_request = {'files': [file], 'depth': depth}
356
343
 
357
- def _get_vulnerabilities_grpc(self, purls: dict) -> dict:
344
+ return self._call_api(
345
+ 'dependencies.GetDependencies',
346
+ self.dependencies_stub.GetDependencies,
347
+ file_request,
348
+ DependencyRequest,
349
+ 'Sending dependency data for decoration (rqId: {rqId})...',
350
+ use_grpc=use_grpc,
351
+ )
352
+
353
+ def get_vulnerabilities_json(self, purls: Optional[dict] = None, use_grpc: Optional[bool] = None) -> Optional[dict]:
358
354
  """
359
355
  Client function to call the rpc for Vulnerability GetVulnerabilities
360
- :param purls: Message to send to the service
361
- :return: Server response or None
362
- """
363
- if not purls:
364
- self.print_stderr('ERROR: No message supplied to send to gRPC service.')
365
- return None
366
- request_id = str(uuid.uuid4())
367
- resp: ComponentsVulnerabilityResponse
368
- try:
369
- request = ParseDict(purls, ComponentsRequest()) # Parse the JSON/Dict into the purl request object
370
- metadata = self.metadata[:]
371
- metadata.append(('x-request-id', request_id)) # Set a Request ID
372
- self.print_debug(f'Sending vulnerability data for decoration (rqId: {request_id})...')
373
- resp = self.vuln_stub.GetComponentsVulnerabilities(request, metadata=metadata, timeout=self.timeout)
374
- except Exception as e:
375
- self.print_stderr(
376
- f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
377
- )
378
- else:
379
- if resp:
380
- if not self._check_status_response(resp.status, request_id):
381
- return None
382
- resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
383
- del resp_dict['status']
384
- return resp_dict
385
- return None
356
+ It will either use REST (default) or gRPC
357
+
358
+ Args:
359
+ purls (dict): Message to send to the service
360
+
361
+ Returns:
362
+ Server response or None
363
+ """
364
+ return self._call_api(
365
+ 'vulnerabilities.GetComponentsVulnerabilities',
366
+ self.vuln_stub.GetComponentsVulnerabilities,
367
+ purls,
368
+ ComponentsRequest,
369
+ 'Sending vulnerability data for decoration (rqId: {rqId})...',
370
+ use_grpc=use_grpc,
371
+ )
386
372
 
387
- def get_semgrep_json(self, purls: dict) -> dict:
373
+ def get_semgrep_json(self, purls: Optional[dict] = None, use_grpc: Optional[bool] = None) -> Optional[dict]:
388
374
  """
389
375
  Client function to call the rpc for Semgrep GetIssues
390
- :param purls: Message to send to the service
391
- :return: Server response or None
392
- """
393
- if not purls:
394
- self.print_stderr('ERROR: No message supplied to send to gRPC service.')
395
- return None
396
- request_id = str(uuid.uuid4())
397
- resp: SemgrepResponse
398
- try:
399
- request = ParseDict(purls, PurlRequest()) # Parse the JSON/Dict into the purl request object
400
- metadata = self.metadata[:]
401
- metadata.append(('x-request-id', request_id)) # Set a Request ID
402
- self.print_debug(f'Sending semgrep data for decoration (rqId: {request_id})...')
403
- resp = self.semgrep_stub.GetIssues(request, metadata=metadata, timeout=self.timeout)
404
- except Exception as e:
405
- self.print_stderr(
406
- f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
407
- )
408
- else:
409
- if resp:
410
- if not self._check_status_response(resp.status, request_id):
411
- return None
412
- resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
413
- del resp_dict['status']
414
- return resp_dict
415
- return None
416
376
 
417
- def search_components_json(self, search: dict) -> dict:
377
+ Args:
378
+ purls (dict): Message to send to the service
379
+ use_grpc (bool): Whether to use gRPC or REST
380
+
381
+ Returns:
382
+ Server response or None
383
+ """
384
+ return self._call_api(
385
+ 'semgrep.GetComponentsIssues',
386
+ self.semgrep_stub.GetComponentsIssues,
387
+ purls,
388
+ ComponentsRequest,
389
+ 'Sending semgrep data for decoration (rqId: {rqId})...',
390
+ use_grpc=use_grpc,
391
+ )
392
+
393
+ def search_components_json(self, search: dict, use_grpc: Optional[bool] = None) -> Optional[dict]:
418
394
  """
419
395
  Client function to call the rpc for Components SearchComponents
420
- :param search: Message to send to the service
421
- :return: Server response or None
422
- """
423
- if not search:
424
- self.print_stderr('ERROR: No message supplied to send to gRPC service.')
425
- return None
426
- request_id = str(uuid.uuid4())
427
- resp: CompSearchResponse
428
- try:
429
- request = ParseDict(search, CompSearchRequest()) # Parse the JSON/Dict into the purl request object
430
- metadata = self.metadata[:]
431
- metadata.append(('x-request-id', request_id)) # Set a Request ID
432
- self.print_debug(f'Sending component search data (rqId: {request_id})...')
433
- resp = self.comp_search_stub.SearchComponents(request, metadata=metadata, timeout=self.timeout)
434
- except Exception as e:
435
- self.print_stderr(
436
- f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
437
- )
438
- else:
439
- if resp:
440
- if not self._check_status_response(resp.status, request_id):
441
- return None
442
- resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
443
- del resp_dict['status']
444
- return resp_dict
445
- return None
446
396
 
447
- def get_component_versions_json(self, search: dict) -> dict:
397
+ Args:
398
+ search (dict): Message to send to the service
399
+ Returns:
400
+ Server response or None
401
+ """
402
+ return self._call_api(
403
+ 'components.SearchComponents',
404
+ self.comp_search_stub.SearchComponents,
405
+ search,
406
+ CompSearchRequest,
407
+ 'Sending component search data for decoration (rqId: {rqId})...',
408
+ use_grpc=use_grpc,
409
+ )
410
+
411
+ def get_component_versions_json(self, search: dict, use_grpc: Optional[bool] = None) -> Optional[dict]:
448
412
  """
449
413
  Client function to call the rpc for Components GetComponentVersions
450
- :param search: Message to send to the service
451
- :return: Server response or None
452
- """
453
- if not search:
454
- self.print_stderr('ERROR: No message supplied to send to gRPC service.')
455
- return None
456
- request_id = str(uuid.uuid4())
457
- resp: CompVersionResponse
458
- try:
459
- request = ParseDict(search, CompVersionRequest()) # Parse the JSON/Dict into the purl request object
460
- metadata = self.metadata[:]
461
- metadata.append(('x-request-id', request_id)) # Set a Request ID
462
- self.print_debug(f'Sending component version data (rqId: {request_id})...')
463
- resp = self.comp_search_stub.GetComponentVersions(request, metadata=metadata, timeout=self.timeout)
464
- except Exception as e:
465
- self.print_stderr(
466
- f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
467
- )
468
- else:
469
- if resp:
470
- if not self._check_status_response(resp.status, request_id):
471
- return None
472
- resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
473
- del resp_dict['status']
474
- return resp_dict
475
- return None
414
+
415
+ Args:
416
+ search (dict): Message to send to the service
417
+ Returns:
418
+ Server response or None
419
+ """
420
+ return self._call_api(
421
+ 'components.GetComponentVersions',
422
+ self.comp_search_stub.GetComponentVersions,
423
+ search,
424
+ CompVersionRequest,
425
+ 'Sending component version data for decoration (rqId: {rqId})...',
426
+ use_grpc=use_grpc,
427
+ )
476
428
 
477
429
  def folder_hash_scan(self, request: Dict) -> Optional[Dict]:
478
430
  """
@@ -491,6 +443,44 @@ class ScanossGrpc(ScanossBase):
491
443
  'Sending folder hash scan data (rqId: {rqId})...',
492
444
  )
493
445
 
446
+ def _call_api(
447
+ self,
448
+ endpoint_key: str,
449
+ rpc_method,
450
+ request_input,
451
+ request_type,
452
+ debug_msg: Optional[str] = None,
453
+ use_grpc: Optional[bool] = None,
454
+ ) -> Optional[Dict]:
455
+ """
456
+ Unified method to call either gRPC or REST API based on configuration
457
+
458
+ Args:
459
+ endpoint_key (str): The key to lookup the REST endpoint in REST_ENDPOINTS
460
+ rpc_method: The gRPC stub method (used only if use_grpc is True)
461
+ request_input: Either a dict or a gRPC request object
462
+ request_type: The type of the gRPC request object (used only if use_grpc is True)
463
+ debug_msg (str, optional): Debug message template that can include {rqId} placeholder
464
+ use_grpc (bool, optional): Override the instance's use_grpc setting. If None, uses self.use_grpc
465
+
466
+ Returns:
467
+ dict: The parsed response as a dictionary, or None if something went wrong
468
+ """
469
+ if not request_input:
470
+ self.print_stderr('ERROR: No message supplied to send to service.')
471
+ return None
472
+
473
+ # Determine whether to use gRPC or REST
474
+ use_grpc_flag = use_grpc if use_grpc is not None else self.use_grpc
475
+
476
+ if use_grpc_flag:
477
+ return self._call_rpc(rpc_method, request_input, request_type, debug_msg)
478
+ else:
479
+ # For REST, we only need the dict input
480
+ if not isinstance(request_input, dict):
481
+ request_input = MessageToDict(request_input, preserving_proto_field_name=True)
482
+ return self._call_rest(endpoint_key, request_input, debug_msg)
483
+
494
484
  def _call_rpc(self, rpc_method, request_input, request_type, debug_msg: Optional[str] = None) -> Optional[Dict]:
495
485
  """
496
486
  Call a gRPC method and return the response as a dictionary
@@ -510,20 +500,21 @@ class ScanossGrpc(ScanossBase):
510
500
  else:
511
501
  request_obj = request_input
512
502
  metadata = self.metadata[:] + [('x-request-id', request_id)]
513
- self.print_debug(debug_msg.format(rqId=request_id))
503
+ if debug_msg:
504
+ self.print_debug(debug_msg.format(rqId=request_id))
514
505
  try:
515
506
  resp = rpc_method(request_obj, metadata=metadata, timeout=self.timeout)
516
507
  except grpc.RpcError as e:
517
508
  raise ScanossGrpcError(
518
509
  f'{e.__class__.__name__} while sending gRPC message (rqId: {request_id}): {e.details()}'
519
510
  )
520
- if resp and not self._check_status_response(resp.status, request_id):
511
+ if resp and not self._check_status_response_grpc(resp.status, request_id):
521
512
  return None
522
513
 
523
514
  resp_dict = MessageToDict(resp, preserving_proto_field_name=True)
524
515
  return resp_dict
525
516
 
526
- def _check_status_response(self, status_response: StatusResponse, request_id: str = None) -> bool:
517
+ def _check_status_response_grpc(self, status_response: StatusResponse, request_id: str = None) -> bool:
527
518
  """
528
519
  Check the response object to see if the command was successful or not
529
520
  :param status_response: Status Response
@@ -538,15 +529,59 @@ class ScanossGrpc(ScanossBase):
538
529
  if status_code > ScanossGrpcStatusCode.SUCCESS:
539
530
  ret_val = False # default to failed
540
531
  msg = 'Unsuccessful'
541
- if status_code == ScanossGrpcStatusCode.SUCCESS_WITH_WARNINGS:
532
+ if status_code == ScanossGrpcStatusCode.SUCCEEDED_WITH_WARNINGS:
542
533
  msg = 'Succeeded with warnings'
543
534
  ret_val = True # No need to fail as it succeeded with warnings
544
- elif status_code == ScanossGrpcStatusCode.FAILED_WITH_WARNINGS:
535
+ elif status_code == ScanossGrpcStatusCode.WARNING:
545
536
  msg = 'Failed with warnings'
546
537
  self.print_stderr(f'{msg} (rqId: {request_id} - status: {status_code}): {status_response.message}')
547
538
  return ret_val
548
539
  return True
549
540
 
541
+ def check_status_response_rest(self, status_dict: dict, request_id: Optional[str] = None) -> bool:
542
+ """
543
+ Check the REST response dictionary to see if the command was successful or not
544
+
545
+ Args:
546
+ status_dict (dict): Status dictionary from REST response containing 'status' and 'message' keys
547
+ request_id (str, optional): Request ID for logging
548
+ Returns:
549
+ bool: True if successful, False otherwise
550
+ """
551
+ if not status_dict:
552
+ self.print_stderr(f'Warning: No status response supplied (rqId: {request_id}). Assuming it was ok.')
553
+ return True
554
+
555
+ if request_id:
556
+ self.print_debug(f'Checking response status (rqId: {request_id}): {status_dict}')
557
+
558
+ # Get status from dictionary - it can be either a string or nested dict
559
+ status = status_dict.get('status')
560
+ message = status_dict.get('message', '')
561
+ ret_val = True
562
+
563
+ # Handle case where status might be a string directly
564
+ if isinstance(status, str):
565
+ status_str = status.upper()
566
+ if status_str == ScanossRESTStatusCode.SUCCESS.value:
567
+ ret_val = True
568
+ elif status_str == ScanossRESTStatusCode.SUCCEEDED_WITH_WARNINGS.value:
569
+ self.print_stderr(f'Succeeded with warnings (rqId: {request_id}): {message}')
570
+ ret_val = True
571
+ elif status_str == ScanossRESTStatusCode.WARNING.value:
572
+ self.print_stderr(f'Failed with warnings (rqId: {request_id}): {message}')
573
+ ret_val = False
574
+ elif status_str == ScanossRESTStatusCode.FAILED.value:
575
+ self.print_stderr(f'Unsuccessful (rqId: {request_id}): {message}')
576
+ ret_val = False
577
+ else:
578
+ self.print_debug(f'Unknown status "{status_str}" (rqId: {request_id}). Assuming success.')
579
+ ret_val = True
580
+
581
+ # Otherwise asume success
582
+ self.print_debug(f'Unexpected status type {type(status)} (rqId: {request_id}). Assuming success.')
583
+ return ret_val
584
+
550
585
  def _get_proxy_config(self):
551
586
  """
552
587
  Set the grpc_proxy/http_proxy/https_proxy environment variables if PAC file has been specified
@@ -569,138 +604,165 @@ class ScanossGrpc(ScanossBase):
569
604
  os.environ['http_proxy'] = proxies.get('http') or ''
570
605
  os.environ['https_proxy'] = proxies.get('https') or ''
571
606
 
572
- def get_provenance_json(self, purls: dict) -> dict:
573
- """
574
- Client function to call the rpc for GetComponentProvenance
575
- :param purls: Message to send to the service
576
- :return: Server response or None
607
+ def get_provenance_json(self, purls: dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
577
608
  """
578
- if not purls:
579
- self.print_stderr('ERROR: No message supplied to send to gRPC service.')
580
- return None
581
- request_id = str(uuid.uuid4())
582
- resp: ContributorResponse
583
- try:
584
- request = ParseDict(purls, PurlRequest()) # Parse the JSON/Dict into the purl request object
585
- metadata = self.metadata[:]
586
- metadata.append(('x-request-id', request_id)) # Set a Request ID
587
- self.print_debug(f'Sending data for provenance decoration (rqId: {request_id})...')
588
- resp = self.provenance_stub.GetComponentContributors(request, metadata=metadata, timeout=self.timeout)
589
- except Exception as e:
590
- self.print_stderr(
591
- f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
592
- )
593
- else:
594
- if resp:
595
- if not self._check_status_response(resp.status, request_id):
596
- return None
597
- resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
598
- return resp_dict
599
- return None
609
+ Client function to call the rpc for GetComponentContributors
610
+
611
+ Args:
612
+ purls (dict): ComponentsRequest
613
+ use_grpc (bool): Whether to use gRPC or REST (None = use instance default)
614
+
615
+ Returns:
616
+ dict: JSON response or None
617
+ """
618
+ return self._call_api(
619
+ 'geoprovenance.GetCountryContributorsByComponents',
620
+ self.provenance_stub.GetCountryContributorsByComponents,
621
+ purls,
622
+ ComponentsRequest,
623
+ 'Sending data for provenance decoration (rqId: {rqId})...',
624
+ use_grpc=use_grpc,
625
+ )
600
626
 
601
- def get_provenance_origin(self, request: Dict) -> Optional[Dict]:
627
+ def get_provenance_origin(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
602
628
  """
603
- Client function to call the rpc for GetComponentOrigin
629
+ Client function to call the rpc for GetOriginByComponents
604
630
 
605
631
  Args:
606
- request (Dict): GetComponentOrigin Request
632
+ request (Dict): GetOriginByComponents Request
607
633
 
608
634
  Returns:
609
635
  Optional[Dict]: OriginResponse, or None if the request was not successfull
610
636
  """
611
- return self._call_rpc(
612
- self.provenance_stub.GetComponentOrigin,
637
+ return self._call_api(
638
+ 'geoprovenance.GetOriginByComponents',
639
+ self.provenance_stub.GetOriginByComponents,
613
640
  request,
614
- PurlRequest,
641
+ ComponentsRequest,
615
642
  'Sending data for provenance origin decoration (rqId: {rqId})...',
643
+ use_grpc=use_grpc,
616
644
  )
617
645
 
618
- def get_crypto_algorithms_for_purl(self, request: Dict) -> Optional[Dict]:
646
+ def get_crypto_algorithms_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
619
647
  """
620
- Client function to call the rpc for GetAlgorithms for a list of purls
648
+ Client function to call the rpc for GetComponentsAlgorithms for a list of purls
621
649
 
622
650
  Args:
623
- request (Dict): PurlRequest
651
+ request (Dict): ComponentsRequest
652
+ use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default)
624
653
 
625
654
  Returns:
626
655
  Optional[Dict]: AlgorithmResponse, or None if the request was not successfull
627
656
  """
628
- return self._call_rpc(
629
- self.crypto_stub.GetAlgorithms,
657
+ return self._call_api(
658
+ 'cryptography.GetComponentsAlgorithms',
659
+ self.crypto_stub.GetComponentsAlgorithms,
630
660
  request,
631
- PurlRequest,
661
+ ComponentsRequest,
632
662
  'Sending data for cryptographic algorithms decoration (rqId: {rqId})...',
663
+ use_grpc=use_grpc,
633
664
  )
634
665
 
635
- def get_crypto_algorithms_in_range_for_purl(self, request: Dict) -> Optional[Dict]:
666
+ def get_crypto_algorithms_in_range_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
636
667
  """
637
- Client function to call the rpc for GetAlgorithmsInRange for a list of purls
668
+ Client function to call the rpc for GetComponentsAlgorithmsInRange for a list of purls
638
669
 
639
670
  Args:
640
- request (Dict): PurlRequest
671
+ request (Dict): ComponentsRequest
672
+ use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default)
641
673
 
642
674
  Returns:
643
675
  Optional[Dict]: AlgorithmsInRangeResponse, or None if the request was not successfull
644
676
  """
645
- return self._call_rpc(
646
- self.crypto_stub.GetAlgorithmsInRange,
677
+ return self._call_api(
678
+ 'cryptography.GetComponentsAlgorithmsInRange',
679
+ self.crypto_stub.GetComponentsAlgorithmsInRange,
647
680
  request,
648
- PurlRequest,
681
+ ComponentsRequest,
649
682
  'Sending data for cryptographic algorithms in range decoration (rqId: {rqId})...',
683
+ use_grpc=use_grpc,
650
684
  )
651
685
 
652
- def get_encryption_hints_for_purl(self, request: Dict) -> Optional[Dict]:
686
+ def get_encryption_hints_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
653
687
  """
654
- Client function to call the rpc for GetEncryptionHints for a list of purls
688
+ Client function to call the rpc for GetComponentsEncryptionHints for a list of purls
655
689
 
656
690
  Args:
657
- request (Dict): PurlRequest
691
+ request (Dict): ComponentsRequest
692
+ use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default)
658
693
 
659
694
  Returns:
660
695
  Optional[Dict]: HintsResponse, or None if the request was not successfull
661
696
  """
662
- return self._call_rpc(
663
- self.crypto_stub.GetEncryptionHints,
697
+ return self._call_api(
698
+ 'cryptography.GetComponentsEncryptionHints',
699
+ self.crypto_stub.GetComponentsEncryptionHints,
664
700
  request,
665
- PurlRequest,
701
+ ComponentsRequest,
666
702
  'Sending data for encryption hints decoration (rqId: {rqId})...',
703
+ use_grpc=use_grpc,
667
704
  )
668
705
 
669
- def get_encryption_hints_in_range_for_purl(self, request: Dict) -> Optional[Dict]:
706
+ def get_encryption_hints_in_range_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
670
707
  """
671
- Client function to call the rpc for GetHintsInRange for a list of purls
708
+ Client function to call the rpc for GetComponentsHintsInRange for a list of purls
672
709
 
673
710
  Args:
674
- request (Dict): PurlRequest
711
+ request (Dict): ComponentsRequest
712
+ use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default)
675
713
 
676
714
  Returns:
677
715
  Optional[Dict]: HintsInRangeResponse, or None if the request was not successfull
678
716
  """
679
- return self._call_rpc(
680
- self.crypto_stub.GetHintsInRange,
717
+ return self._call_api(
718
+ 'cryptography.GetComponentsHintsInRange',
719
+ self.crypto_stub.GetComponentsHintsInRange,
681
720
  request,
682
- PurlRequest,
721
+ ComponentsRequest,
683
722
  'Sending data for encryption hints in range decoration (rqId: {rqId})...',
723
+ use_grpc=use_grpc,
684
724
  )
685
725
 
686
- def get_versions_in_range_for_purl(self, request: Dict) -> Optional[Dict]:
726
+ def get_versions_in_range_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
687
727
  """
688
- Client function to call the rpc for GetVersionsInRange for a list of purls
728
+ Client function to call the rpc for GetComponentsVersionsInRange for a list of purls
689
729
 
690
730
  Args:
691
- request (Dict): PurlRequest
731
+ request (Dict): ComponentsRequest
732
+ use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default)
692
733
 
693
734
  Returns:
694
735
  Optional[Dict]: VersionsInRangeResponse, or None if the request was not successfull
695
736
  """
696
- return self._call_rpc(
697
- self.crypto_stub.GetVersionsInRange,
737
+ return self._call_api(
738
+ 'cryptography.GetComponentsVersionsInRange',
739
+ self.crypto_stub.GetComponentsVersionsInRange,
698
740
  request,
699
- PurlRequest,
741
+ ComponentsRequest,
700
742
  'Sending data for cryptographic versions in range decoration (rqId: {rqId})...',
743
+ use_grpc=use_grpc,
744
+ )
745
+
746
+ def get_licenses(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
747
+ """
748
+ Client function to call the rpc for Licenses GetComponentsLicenses
749
+ It will either use REST (default) or gRPC depending on the use_grpc flag
750
+
751
+ Args:
752
+ request (Dict): ComponentsRequest
753
+ Returns:
754
+ Optional[Dict]: ComponentsLicenseResponse, or None if the request was not successfull
755
+ """
756
+ return self._call_api(
757
+ 'licenses.GetComponentsLicenses',
758
+ self.license_stub.GetComponentsLicenses,
759
+ request,
760
+ ComponentsRequest,
761
+ 'Sending data for license decoration (rqId: {rqId})...',
762
+ use_grpc=use_grpc,
701
763
  )
702
764
 
703
- def load_generic_headers(self, url):
765
+ def load_generic_headers(self, url: Optional[str] = None):
704
766
  """
705
767
  Adds custom headers from req_headers to metadata.
706
768
 
@@ -722,32 +784,91 @@ class ScanossGrpc(ScanossBase):
722
784
  # Start of REST Client Functions
723
785
  #
724
786
 
725
- def rest_post(self, uri: str, request_id: str, data: dict) -> dict:
787
+ def _rest_get(self, uri: str, request_id: str, params: Optional[dict] = None) -> Optional[dict]:
788
+ """
789
+ Send a GET request to the specified URI with optional query parameters.
790
+
791
+ Args:
792
+ uri (str): URI to send GET request to
793
+ request_id (str): request id
794
+ params (dict, optional): Optional query parameters as dictionary
795
+
796
+ Returns:
797
+ dict: JSON response or None
798
+ """
799
+ if not uri:
800
+ self.print_stderr('Error: Missing URI. Cannot perform GET request.')
801
+ return None
802
+ self.print_trace(f'Sending REST GET request to {uri}...')
803
+ headers = self.headers.copy()
804
+ headers['x-request-id'] = request_id
805
+ retry = 0
806
+ while retry <= self.retry_limit:
807
+ retry += 1
808
+ try:
809
+ response = self.session.get(uri, headers=headers, params=params, timeout=self.timeout)
810
+ response.raise_for_status() # Raises an HTTPError for bad responses
811
+ return response.json()
812
+ except (requests.exceptions.SSLError, requests.exceptions.ProxyError) as e:
813
+ self.print_stderr(f'ERROR: Exception ({e.__class__.__name__}) sending GET request - {e}.')
814
+ raise Exception(f'ERROR: The SCANOSS API GET request failed for {uri}') from e
815
+ except requests.exceptions.HTTPError as e:
816
+ self.print_stderr(f'ERROR: HTTP error sending GET request ({request_id}): {e}')
817
+ raise Exception(
818
+ f'ERROR: The SCANOSS API GET request failed with status {e.response.status_code} for {uri}'
819
+ ) from e
820
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
821
+ if retry > self.retry_limit: # Timed out retry_limit or more times, fail
822
+ self.print_stderr(f'ERROR: {e.__class__.__name__} sending GET request ({request_id}): {e}')
823
+ raise Exception(
824
+ f'ERROR: The SCANOSS API GET request timed out ({e.__class__.__name__}) for {uri}'
825
+ ) from e
826
+ else:
827
+ self.print_stderr(f'Warning: {e.__class__.__name__} communicating with {self.url}. Retrying...')
828
+ time.sleep(5)
829
+ except requests.exceptions.RequestException as e:
830
+ self.print_stderr(f'Error: Problem sending GET request to {uri}: {e}')
831
+ raise Exception(f'ERROR: The SCANOSS API GET request failed for {uri}') from e
832
+ except Exception as e:
833
+ self.print_stderr(
834
+ f'ERROR: Exception ({e.__class__.__name__}) sending GET request ({request_id}) to {uri}: {e}'
835
+ )
836
+ raise Exception(f'ERROR: The SCANOSS API GET request failed for {uri}') from e
837
+ return None
838
+
839
+ def _rest_post(self, uri: str, request_id: str, data: dict) -> Optional[dict]:
726
840
  """
727
841
  Post the specified data to the given URI.
728
- :param request_id: request id
729
- :param uri: URI to post to
730
- :param data: json data to post
731
- :return: JSON response or None
842
+
843
+ Args:
844
+ uri (str): URI to post to
845
+ request_id (str): request id
846
+ data (dict): json data to post
847
+
848
+ Returns:
849
+ dict: JSON response or None
732
850
  """
733
851
  if not uri:
734
852
  self.print_stderr('Error: Missing URI. Cannot search for project.')
735
853
  return None
736
854
  self.print_trace(f'Sending REST POST data to {uri}...')
737
- headers = self.headers
738
- headers['x-request-id'] = request_id # send a unique request id for each post
739
- retry = 0 # Add some retry logic to cater for timeouts, etc.
855
+ headers = self.headers.copy()
856
+ headers['x-request-id'] = request_id
857
+ retry = 0
740
858
  while retry <= self.retry_limit:
741
859
  retry += 1
742
860
  try:
743
861
  response = self.session.post(uri, headers=headers, json=data, timeout=self.timeout)
744
862
  response.raise_for_status() # Raises an HTTPError for bad responses
745
863
  return response.json()
746
- except requests.exceptions.RequestException as e:
747
- self.print_stderr(f'Error: Problem posting data to {uri}: {e}')
748
864
  except (requests.exceptions.SSLError, requests.exceptions.ProxyError) as e:
749
865
  self.print_stderr(f'ERROR: Exception ({e.__class__.__name__}) POSTing data - {e}.')
750
866
  raise Exception(f'ERROR: The SCANOSS Decoration API request failed for {uri}') from e
867
+ except requests.exceptions.HTTPError as e:
868
+ self.print_stderr(f'ERROR: HTTP error POSTing data ({request_id}): {e}')
869
+ raise Exception(
870
+ f'ERROR: The SCANOSS Decoration API request failed with status {e.response.status_code} for {uri}'
871
+ ) from e
751
872
  except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
752
873
  if retry > self.retry_limit: # Timed out retry_limit or more times, fail
753
874
  self.print_stderr(f'ERROR: {e.__class__.__name__} POSTing decoration data ({request_id}): {e}')
@@ -757,6 +878,9 @@ class ScanossGrpc(ScanossBase):
757
878
  else:
758
879
  self.print_stderr(f'Warning: {e.__class__.__name__} communicating with {self.url}. Retrying...')
759
880
  time.sleep(5)
881
+ except requests.exceptions.RequestException as e:
882
+ self.print_stderr(f'Error: Problem posting data to {uri}: {e}')
883
+ raise Exception(f'ERROR: The SCANOSS Decoration API request failed for {uri}') from e
760
884
  except Exception as e:
761
885
  self.print_stderr(
762
886
  f'ERROR: Exception ({e.__class__.__name__}) POSTing data ({request_id}) to {uri}: {e}'
@@ -764,52 +888,46 @@ class ScanossGrpc(ScanossBase):
764
888
  raise Exception(f'ERROR: The SCANOSS Decoration API request failed for {uri}') from e
765
889
  return None
766
890
 
767
- def _get_vulnerabilities_rest(self, purls: dict):
891
+ def _call_rest(self, endpoint_key: str, request_input: dict, debug_msg: Optional[str] = None) -> Optional[Dict]:
768
892
  """
769
- Get the vulnerabilities for the given purls using REST API
770
- :param purls: Purl Request dictionary
771
- :return: Vulnerability Response, or None if the request was unsuccessful
772
- """
773
- if not purls:
774
- self.print_stderr('ERROR: No message supplied to send to REST decoration service.')
775
- return None
776
- request_id = str(uuid.uuid4())
777
- self.print_debug(f'Sending data for Vulnerabilities via REST (request id: {request_id})...')
778
- response = self.rest_post(f'{self.orig_url}{DEFAULT_URI_PREFIX}/vulnerabilities/components', request_id, purls)
779
- self.print_trace(f'Received response for Vulnerabilities via REST (request id: {request_id}): {response}')
780
- if response:
781
- # Parse the JSON/Dict into the purl response
782
- resp_obj = ParseDict(response, ComponentsVulnerabilityResponse(), True)
783
- if resp_obj:
784
- self.print_debug(f'Vulnerability Response: {resp_obj}')
785
- if not self._check_status_response(resp_obj.status, request_id):
786
- return None
787
- del response['status']
788
- return response
789
- return None
893
+ Call a REST endpoint and return the response as a dictionary
790
894
 
791
- def _process_dep_file_rest(self, file, depth: int = 1) -> dict:
792
- """
793
- Porcess a single dependency file using REST
895
+ Args:
896
+ endpoint_key (str): The key to lookup the REST endpoint in REST_ENDPOINTS
897
+ request_input (dict): The request data to send
898
+ debug_msg (str, optional): Debug message template that can include {rqId} placeholder.
794
899
 
795
- :param file: dependency file purls
796
- :param depth: depth to search (default: 1)
797
- :return: response JSON or None
900
+ Returns:
901
+ dict: The parsed REST response as a dictionary, or None if something went wrong
798
902
  """
903
+ if endpoint_key not in REST_ENDPOINTS:
904
+ raise ScanossGrpcError(f'Unknown REST endpoint key: {endpoint_key}')
905
+
906
+ endpoint_config = REST_ENDPOINTS[endpoint_key]
907
+ endpoint_path = endpoint_config['path']
908
+ method = endpoint_config['method']
909
+ endpoint_url = f'{self.orig_url}{DEFAULT_URI_PREFIX}{endpoint_path}'
799
910
  request_id = str(uuid.uuid4())
800
- self.print_debug(f'Sending data for Dependencies via REST (request id: {request_id})...')
801
- file_request = {'files': [file], 'depth': depth}
802
- response = self.rest_post(f'{self.orig_url}{DEFAULT_URI_PREFIX}/dependencies/dependencies',
803
- request_id, file_request
804
- )
805
- self.print_trace(f'Received response for Dependencies via REST (request id: {request_id}): {response}')
806
- if response:
807
- return response
808
- return None
911
+
912
+ if debug_msg:
913
+ self.print_debug(debug_msg.format(rqId=request_id))
914
+
915
+ if method == 'GET':
916
+ response = self._rest_get(endpoint_url, request_id, params=request_input)
917
+ else: # POST
918
+ response = self._rest_post(endpoint_url, request_id, request_input)
919
+
920
+ if response and 'status' in response and not self.check_status_response_rest(response['status'], request_id):
921
+ return None
922
+
923
+ return response
924
+
925
+
809
926
  #
810
927
  # End of ScanossGrpc Class
811
928
  #
812
929
 
930
+
813
931
  @dataclass
814
932
  class GrpcConfig:
815
933
  url: str = DEFAULT_URL
@@ -840,6 +958,7 @@ def create_grpc_config_from_args(args) -> GrpcConfig:
840
958
  grpc_proxy=getattr(args, 'grpc_proxy', None),
841
959
  )
842
960
 
961
+
843
962
  #
844
963
  # End of GrpcConfig class
845
- #
964
+ #