Skip to content

Commit 11ef5b1

Browse files
authored
Merge pull request #108 from scanoss/fix/mdaloia/SP-2195-error-during-dependency-evaluation
fix: SP-2195 timeout error during dependency scan
2 parents aa13431 + 46f693d commit 11ef5b1

File tree

2 files changed

+93
-53
lines changed

2 files changed

+93
-53
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Upcoming changes...
1111

1212
## [1.20.5] - 2025-03-13
13+
### Fixed
14+
- Fixed timeout issue with dependency scan
1315
### Added
1416
- Improved documentation on spdxlite.py file
1517
### Modified

Diff for: src/scanoss/scanossgrpc.py

+91-53
Original file line numberDiff line numberDiff line change
@@ -22,50 +22,58 @@
2222
THE SOFTWARE.
2323
"""
2424

25+
import concurrent.futures
26+
import json
2527
import os
2628
import uuid
29+
from urllib.parse import urlparse
2730

2831
import grpc
29-
import json
30-
3132
from google.protobuf.json_format import MessageToDict, ParseDict
3233
from pypac.parser import PACFile
3334
from pypac.resolver import ProxyResolver
34-
from urllib.parse import urlparse
3535

36-
from .api.components.v2.scanoss_components_pb2_grpc import ComponentsStub
37-
from .api.cryptography.v2.scanoss_cryptography_pb2_grpc import CryptographyStub
38-
from .api.dependencies.v2.scanoss_dependencies_pb2_grpc import DependenciesStub
39-
from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2_grpc import VulnerabilitiesStub
40-
from .api.provenance.v2.scanoss_provenance_pb2_grpc import ProvenanceStub
41-
from .api.semgrep.v2.scanoss_semgrep_pb2_grpc import SemgrepStub
42-
from .api.cryptography.v2.scanoss_cryptography_pb2 import AlgorithmResponse
43-
from .api.dependencies.v2.scanoss_dependencies_pb2 import DependencyRequest, DependencyResponse
44-
from .api.common.v2.scanoss_common_pb2 import EchoRequest, EchoResponse, StatusResponse, StatusCode, PurlRequest
45-
from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2 import VulnerabilityResponse
46-
from .api.semgrep.v2.scanoss_semgrep_pb2 import SemgrepResponse
36+
from . import __version__
37+
from .api.common.v2.scanoss_common_pb2 import (
38+
EchoRequest,
39+
EchoResponse,
40+
PurlRequest,
41+
StatusCode,
42+
StatusResponse,
43+
)
4744
from .api.components.v2.scanoss_components_pb2 import (
4845
CompSearchRequest,
4946
CompSearchResponse,
5047
CompVersionRequest,
5148
CompVersionResponse,
5249
)
50+
from .api.components.v2.scanoss_components_pb2_grpc import ComponentsStub
51+
from .api.cryptography.v2.scanoss_cryptography_pb2 import AlgorithmResponse
52+
from .api.cryptography.v2.scanoss_cryptography_pb2_grpc import CryptographyStub
53+
from .api.dependencies.v2.scanoss_dependencies_pb2 import DependencyRequest
54+
from .api.dependencies.v2.scanoss_dependencies_pb2_grpc import DependenciesStub
5355
from .api.provenance.v2.scanoss_provenance_pb2 import ProvenanceResponse
56+
from .api.provenance.v2.scanoss_provenance_pb2_grpc import ProvenanceStub
57+
from .api.semgrep.v2.scanoss_semgrep_pb2 import SemgrepResponse
58+
from .api.semgrep.v2.scanoss_semgrep_pb2_grpc import SemgrepStub
59+
from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2 import VulnerabilityResponse
60+
from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2_grpc import VulnerabilitiesStub
5461
from .scanossbase import ScanossBase
55-
from . import __version__
5662

5763
DEFAULT_URL = 'https://api.osskb.org' # default free service URL
5864
DEFAULT_URL2 = 'https://api.scanoss.com' # default premium service URL
5965
SCANOSS_GRPC_URL = os.environ.get('SCANOSS_GRPC_URL') if os.environ.get('SCANOSS_GRPC_URL') else DEFAULT_URL
6066
SCANOSS_API_KEY = os.environ.get('SCANOSS_API_KEY') if os.environ.get('SCANOSS_API_KEY') else ''
6167

68+
MAX_CONCURRENT_REQUESTS = 5
69+
6270

6371
class ScanossGrpc(ScanossBase):
6472
"""
6573
Client for gRPC functionality
6674
"""
6775

68-
def __init__(
76+
def __init__( # noqa: PLR0913
6977
self,
7078
url: str = None,
7179
debug: bool = False,
@@ -222,31 +230,54 @@ def get_dependencies_json(self, dependencies: dict, depth: int = 1) -> dict:
222230
:return: Server response or None
223231
"""
224232
if not dependencies:
225-
self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
233+
self.print_stderr('ERROR: No message supplied to send to gRPC service.')
226234
return None
227-
request_id = str(uuid.uuid4())
228-
resp: DependencyResponse
229-
try:
230-
files_json = dependencies.get('files')
231-
if files_json is None or len(files_json) == 0:
232-
self.print_stderr(f'ERROR: No dependency data supplied to send to gRPC service.')
235+
236+
files_json = dependencies.get('files')
237+
238+
if files_json is None or len(files_json) == 0:
239+
self.print_stderr('ERROR: No dependency data supplied to send to gRPC service.')
240+
return None
241+
242+
def process_file(file):
243+
request_id = str(uuid.uuid4())
244+
try:
245+
file_request = {'files': [file]}
246+
247+
request = ParseDict(file_request, DependencyRequest())
248+
request.depth = depth
249+
metadata = self.metadata[:]
250+
metadata.append(('x-request-id', request_id))
251+
self.print_debug(f'Sending dependency data for decoration (rqId: {request_id})...')
252+
resp = self.dependencies_stub.GetDependencies(request, metadata=metadata, timeout=self.timeout)
253+
254+
return MessageToDict(resp, preserving_proto_field_name=True)
255+
except Exception as e:
256+
self.print_stderr(
257+
f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
258+
)
233259
return None
234-
request = ParseDict(dependencies, DependencyRequest()) # Parse the JSON/Dict into the dependency object
235-
request.depth = depth
236-
metadata = self.metadata[:]
237-
metadata.append(('x-request-id', request_id)) # Set a Request ID
238-
self.print_debug(f'Sending dependency data for decoration (rqId: {request_id})...')
239-
resp = self.dependencies_stub.GetDependencies(request, metadata=metadata, timeout=self.timeout)
240-
except Exception as e:
241-
self.print_stderr(
242-
f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
243-
)
244-
else:
245-
if resp:
246-
if not self._check_status_response(resp.status, request_id):
247-
return None
248-
return MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dictionary
249-
return None
260+
261+
all_responses = []
262+
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_CONCURRENT_REQUESTS) as executor:
263+
future_to_file = {executor.submit(process_file, file): file for file in files_json}
264+
265+
for future in concurrent.futures.as_completed(future_to_file):
266+
response = future.result()
267+
if response:
268+
all_responses.append(response)
269+
270+
SUCCESS_STATUS = 'SUCCESS'
271+
272+
merged_response = {'files': [], 'status': {'status': SUCCESS_STATUS, 'message': 'Success'}}
273+
for response in all_responses:
274+
if response:
275+
if 'files' in response and len(response['files']) > 0:
276+
merged_response['files'].append(response['files'][0])
277+
# Overwrite the status if the any of the responses was not successful
278+
if 'status' in response and response['status']['status'] != SUCCESS_STATUS:
279+
merged_response['status'] = response['status']
280+
return merged_response
250281

251282
def get_crypto_json(self, purls: dict) -> dict:
252283
"""
@@ -255,7 +286,7 @@ def get_crypto_json(self, purls: dict) -> dict:
255286
:return: Server response or None
256287
"""
257288
if not purls:
258-
self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
289+
self.print_stderr('ERROR: No message supplied to send to gRPC service.')
259290
return None
260291
request_id = str(uuid.uuid4())
261292
resp: AlgorithmResponse
@@ -285,7 +316,7 @@ def get_vulnerabilities_json(self, purls: dict) -> dict:
285316
:return: Server response or None
286317
"""
287318
if not purls:
288-
self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
319+
self.print_stderr('ERROR: No message supplied to send to gRPC service.')
289320
return None
290321
request_id = str(uuid.uuid4())
291322
resp: VulnerabilityResponse
@@ -315,7 +346,7 @@ def get_semgrep_json(self, purls: dict) -> dict:
315346
:return: Server response or None
316347
"""
317348
if not purls:
318-
self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
349+
self.print_stderr('ERROR: No message supplied to send to gRPC service.')
319350
return None
320351
request_id = str(uuid.uuid4())
321352
resp: SemgrepResponse
@@ -345,7 +376,7 @@ def search_components_json(self, search: dict) -> dict:
345376
:return: Server response or None
346377
"""
347378
if not search:
348-
self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
379+
self.print_stderr('ERROR: No message supplied to send to gRPC service.')
349380
return None
350381
request_id = str(uuid.uuid4())
351382
resp: CompSearchResponse
@@ -375,7 +406,7 @@ def get_component_versions_json(self, search: dict) -> dict:
375406
:return: Server response or None
376407
"""
377408
if not search:
378-
self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
409+
self.print_stderr('ERROR: No message supplied to send to gRPC service.')
379410
return None
380411
request_id = str(uuid.uuid4())
381412
resp: CompVersionResponse
@@ -404,18 +435,22 @@ def _check_status_response(self, status_response: StatusResponse, request_id: st
404435
:param status_response: Status Response
405436
:return: True if successful, False otherwise
406437
"""
438+
439+
SUCCEDED_WITH_WARNINGS_STATUS_CODE = 2
440+
FAILED_STATUS_CODE = 3
441+
407442
if not status_response:
408443
self.print_stderr(f'Warning: No status response supplied (rqId: {request_id}). Assuming it was ok.')
409444
return True
410445
self.print_debug(f'Checking response status (rqId: {request_id}): {status_response}')
411446
status_code: StatusCode = status_response.status
412447
if status_code > 1:
413448
ret_val = False # default to failed
414-
msg = "Unsuccessful"
415-
if status_code == 2:
416-
msg = "Succeeded with warnings"
449+
msg = 'Unsuccessful'
450+
if status_code == SUCCEDED_WITH_WARNINGS_STATUS_CODE:
451+
msg = 'Succeeded with warnings'
417452
ret_val = True # No need to fail as it succeeded with warnings
418-
elif status_code == 3:
453+
elif status_code == FAILED_STATUS_CODE:
419454
msg = 'Failed with warnings'
420455
self.print_stderr(f'{msg} (rqId: {request_id} - status: {status_code}): {status_response.message}')
421456
return ret_val
@@ -428,10 +463,10 @@ def _get_proxy_config(self):
428463
:param self:
429464
"""
430465
if self.grpc_proxy:
431-
self.print_debug(f'Setting GRPC (grpc_proxy) proxy...')
466+
self.print_debug('Setting GRPC (grpc_proxy) proxy...')
432467
os.environ['grpc_proxy'] = self.grpc_proxy
433468
elif self.proxy:
434-
self.print_debug(f'Setting GRPC (http_proxy/https_proxy) proxies...')
469+
self.print_debug('Setting GRPC (http_proxy/https_proxy) proxies...')
435470
os.environ['http_proxy'] = self.proxy
436471
os.environ['https_proxy'] = self.proxy
437472
elif self.pac:
@@ -450,7 +485,7 @@ def get_provenance_json(self, purls: dict) -> dict:
450485
:return: Server response or None
451486
"""
452487
if not purls:
453-
self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
488+
self.print_stderr('ERROR: No message supplied to send to gRPC service.')
454489
return None
455490
request_id = str(uuid.uuid4())
456491
resp: ProvenanceResponse
@@ -461,15 +496,18 @@ def get_provenance_json(self, purls: dict) -> dict:
461496
self.print_debug(f'Sending data for provenance decoration (rqId: {request_id})...')
462497
resp = self.provenance_stub.GetComponentProvenance(request, metadata=metadata, timeout=self.timeout)
463498
except Exception as e:
464-
self.print_stderr(f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message '
465-
f'(rqId: {request_id}): {e}')
499+
self.print_stderr(
500+
f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message (rqId: {request_id}): {e}'
501+
)
466502
else:
467503
if resp:
468504
if not self._check_status_response(resp.status, request_id):
469505
return None
470506
resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
471507
return resp_dict
472508
return None
509+
510+
473511
#
474512
# End of ScanossGrpc Class
475513
#

0 commit comments

Comments
 (0)