scanoss 1.12.2__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 (109) hide show
  1. protoc_gen_swagger/__init__.py +13 -13
  2. protoc_gen_swagger/options/__init__.py +13 -13
  3. protoc_gen_swagger/options/annotations_pb2.py +18 -12
  4. protoc_gen_swagger/options/annotations_pb2.pyi +48 -0
  5. protoc_gen_swagger/options/annotations_pb2_grpc.py +20 -0
  6. protoc_gen_swagger/options/openapiv2_pb2.py +110 -99
  7. protoc_gen_swagger/options/openapiv2_pb2.pyi +1317 -0
  8. protoc_gen_swagger/options/openapiv2_pb2_grpc.py +20 -0
  9. scanoss/__init__.py +18 -18
  10. scanoss/api/__init__.py +17 -17
  11. scanoss/api/common/__init__.py +17 -17
  12. scanoss/api/common/v2/__init__.py +17 -17
  13. scanoss/api/common/v2/scanoss_common_pb2.py +49 -20
  14. scanoss/api/common/v2/scanoss_common_pb2_grpc.py +25 -0
  15. scanoss/api/components/__init__.py +17 -17
  16. scanoss/api/components/v2/__init__.py +17 -17
  17. scanoss/api/components/v2/scanoss_components_pb2.py +68 -43
  18. scanoss/api/components/v2/scanoss_components_pb2_grpc.py +83 -22
  19. scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py +136 -21
  20. scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py +766 -13
  21. scanoss/api/dependencies/__init__.py +17 -17
  22. scanoss/api/dependencies/v2/__init__.py +17 -17
  23. scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +56 -29
  24. scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +94 -8
  25. scanoss/api/geoprovenance/__init__.py +23 -0
  26. scanoss/api/geoprovenance/v2/__init__.py +23 -0
  27. scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2.py +92 -0
  28. scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py +381 -0
  29. scanoss/api/licenses/__init__.py +23 -0
  30. scanoss/api/licenses/v2/__init__.py +23 -0
  31. scanoss/api/licenses/v2/scanoss_licenses_pb2.py +84 -0
  32. scanoss/api/licenses/v2/scanoss_licenses_pb2_grpc.py +302 -0
  33. scanoss/api/scanning/__init__.py +17 -17
  34. scanoss/api/scanning/v2/__init__.py +17 -17
  35. scanoss/api/scanning/v2/scanoss_scanning_pb2.py +42 -13
  36. scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py +86 -7
  37. scanoss/api/semgrep/__init__.py +17 -17
  38. scanoss/api/semgrep/v2/__init__.py +17 -17
  39. scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py +50 -23
  40. scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py +151 -16
  41. scanoss/api/vulnerabilities/__init__.py +17 -17
  42. scanoss/api/vulnerabilities/v2/__init__.py +17 -17
  43. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py +78 -31
  44. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py +282 -18
  45. scanoss/cli.py +2359 -370
  46. scanoss/components.py +187 -94
  47. scanoss/constants.py +22 -0
  48. scanoss/cryptography.py +308 -0
  49. scanoss/csvoutput.py +91 -58
  50. scanoss/cyclonedx.py +221 -63
  51. scanoss/data/build_date.txt +1 -1
  52. scanoss/data/osadl-copyleft.json +133 -0
  53. scanoss/data/scanoss-settings-schema.json +254 -0
  54. scanoss/delta.py +197 -0
  55. scanoss/export/__init__.py +23 -0
  56. scanoss/export/dependency_track.py +227 -0
  57. scanoss/file_filters.py +582 -0
  58. scanoss/filecount.py +75 -69
  59. scanoss/gitlabqualityreport.py +214 -0
  60. scanoss/header_filter.py +563 -0
  61. scanoss/inspection/__init__.py +23 -0
  62. scanoss/inspection/policy_check/__init__.py +0 -0
  63. scanoss/inspection/policy_check/dependency_track/__init__.py +0 -0
  64. scanoss/inspection/policy_check/dependency_track/project_violation.py +479 -0
  65. scanoss/inspection/policy_check/policy_check.py +222 -0
  66. scanoss/inspection/policy_check/scanoss/__init__.py +0 -0
  67. scanoss/inspection/policy_check/scanoss/copyleft.py +243 -0
  68. scanoss/inspection/policy_check/scanoss/undeclared_component.py +309 -0
  69. scanoss/inspection/summary/__init__.py +0 -0
  70. scanoss/inspection/summary/component_summary.py +170 -0
  71. scanoss/inspection/summary/license_summary.py +191 -0
  72. scanoss/inspection/summary/match_summary.py +341 -0
  73. scanoss/inspection/utils/file_utils.py +44 -0
  74. scanoss/inspection/utils/license_utils.py +123 -0
  75. scanoss/inspection/utils/markdown_utils.py +63 -0
  76. scanoss/inspection/utils/scan_result_processor.py +417 -0
  77. scanoss/osadl.py +125 -0
  78. scanoss/results.py +275 -0
  79. scanoss/scancodedeps.py +87 -38
  80. scanoss/scanner.py +431 -539
  81. scanoss/scanners/__init__.py +23 -0
  82. scanoss/scanners/container_scanner.py +476 -0
  83. scanoss/scanners/folder_hasher.py +358 -0
  84. scanoss/scanners/scanner_config.py +73 -0
  85. scanoss/scanners/scanner_hfh.py +252 -0
  86. scanoss/scanoss_settings.py +337 -0
  87. scanoss/scanossapi.py +140 -101
  88. scanoss/scanossbase.py +59 -22
  89. scanoss/scanossgrpc.py +799 -251
  90. scanoss/scanpostprocessor.py +294 -0
  91. scanoss/scantype.py +22 -21
  92. scanoss/services/dependency_track_service.py +132 -0
  93. scanoss/spdxlite.py +532 -174
  94. scanoss/threadeddependencies.py +148 -47
  95. scanoss/threadedscanning.py +53 -37
  96. scanoss/utils/__init__.py +23 -0
  97. scanoss/utils/abstract_presenter.py +103 -0
  98. scanoss/utils/crc64.py +96 -0
  99. scanoss/utils/file.py +84 -0
  100. scanoss/utils/scanoss_scan_results_utils.py +41 -0
  101. scanoss/utils/simhash.py +198 -0
  102. scanoss/winnowing.py +241 -63
  103. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/METADATA +18 -9
  104. scanoss-1.43.1.dist-info/RECORD +110 -0
  105. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/WHEEL +1 -1
  106. scanoss-1.12.2.dist-info/RECORD +0 -58
  107. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/entry_points.txt +0 -0
  108. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info/licenses}/LICENSE +0 -0
  109. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/top_level.txt +0 -0
scanoss/scanossgrpc.py CHANGED
@@ -1,57 +1,137 @@
1
1
  """
2
- SPDX-License-Identifier: MIT
3
-
4
- Copyright (c) 2021, SCANOSS
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in
14
- all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
- THE SOFTWARE.
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2021, SCANOSS
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
23
23
  """
24
24
 
25
+ import concurrent.futures
26
+ import http.client as http_client
27
+ import logging
25
28
  import os
29
+ import sys
30
+ import time
26
31
  import uuid
32
+ from dataclasses import dataclass
33
+ from enum import Enum, IntEnum
34
+ from typing import Dict, Optional
35
+ from urllib.parse import urlparse
27
36
 
28
37
  import grpc
29
- import json
30
-
38
+ import requests
39
+ import urllib3
31
40
  from google.protobuf.json_format import MessageToDict, ParseDict
41
+ from pypac import PACSession
32
42
  from pypac.parser import PACFile
33
43
  from pypac.resolver import ProxyResolver
34
- from urllib.parse import urlparse
44
+ from urllib3.exceptions import InsecureRequestWarning
35
45
 
46
+ from scanoss.api.licenses.v2.scanoss_licenses_pb2_grpc import LicenseStub
47
+ from scanoss.api.scanning.v2.scanoss_scanning_pb2_grpc import ScanningStub
48
+ from scanoss.constants import DEFAULT_TIMEOUT
49
+
50
+ from . import __version__
51
+ from .api.common.v2.scanoss_common_pb2 import (
52
+ ComponentsRequest,
53
+ EchoRequest,
54
+ StatusCode,
55
+ StatusResponse,
56
+ )
57
+ from .api.components.v2.scanoss_components_pb2 import (
58
+ CompSearchRequest,
59
+ CompVersionRequest,
60
+ )
36
61
  from .api.components.v2.scanoss_components_pb2_grpc import ComponentsStub
37
62
  from .api.cryptography.v2.scanoss_cryptography_pb2_grpc import CryptographyStub
63
+ from .api.dependencies.v2.scanoss_dependencies_pb2 import DependencyRequest
38
64
  from .api.dependencies.v2.scanoss_dependencies_pb2_grpc import DependenciesStub
39
- from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2_grpc import VulnerabilitiesStub
65
+ from .api.geoprovenance.v2.scanoss_geoprovenance_pb2_grpc import GeoProvenanceStub
66
+ from .api.scanning.v2.scanoss_scanning_pb2 import HFHRequest
40
67
  from .api.semgrep.v2.scanoss_semgrep_pb2_grpc import SemgrepStub
41
- from .api.cryptography.v2.scanoss_cryptography_pb2 import AlgorithmResponse
42
- from .api.dependencies.v2.scanoss_dependencies_pb2 import DependencyRequest, DependencyResponse
43
- from .api.common.v2.scanoss_common_pb2 import EchoRequest, EchoResponse, StatusResponse, StatusCode, PurlRequest
44
- from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2 import VulnerabilityResponse
45
- from .api.semgrep.v2.scanoss_semgrep_pb2 import SemgrepResponse
46
- from .api.components.v2.scanoss_components_pb2 import (CompSearchRequest, CompSearchResponse,
47
- CompVersionRequest, CompVersionResponse)
68
+ from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2_grpc import VulnerabilitiesStub
48
69
  from .scanossbase import ScanossBase
49
- from . import __version__
50
70
 
51
- DEFAULT_URL = "https://api.osskb.org" # default free service URL
52
- DEFAULT_URL2 = "https://api.scanoss.com" # default premium service URL
53
- SCANOSS_GRPC_URL = os.environ.get("SCANOSS_GRPC_URL") if os.environ.get("SCANOSS_GRPC_URL") else DEFAULT_URL
54
- SCANOSS_API_KEY = os.environ.get("SCANOSS_API_KEY") if os.environ.get("SCANOSS_API_KEY") else ''
71
+ DEFAULT_URL = 'https://api.osskb.org' # default free service URL
72
+ DEFAULT_URL2 = 'https://api.scanoss.com' # default premium service URL
73
+ SCANOSS_GRPC_URL = os.environ.get('SCANOSS_GRPC_URL') if os.environ.get('SCANOSS_GRPC_URL') else DEFAULT_URL
74
+ SCANOSS_API_KEY = os.environ.get('SCANOSS_API_KEY') if os.environ.get('SCANOSS_API_KEY') else ''
75
+ DEFAULT_URI_PREFIX = '/v2'
76
+
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
+ }
107
+
108
+
109
+ class ScanossGrpcError(Exception):
110
+ """
111
+ Custom exception for SCANOSS gRPC errors
112
+ """
113
+
114
+ pass
115
+
116
+
117
+ class ScanossGrpcStatusCode(IntEnum):
118
+ """Status codes for SCANOSS gRPC responses"""
119
+
120
+ UNSPECIFIED = 0
121
+ SUCCESS = 1
122
+ SUCCEEDED_WITH_WARNINGS = 2
123
+ WARNING = 3
124
+ FAILED = 4
125
+
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'
55
135
 
56
136
 
57
137
  class ScanossGrpc(ScanossBase):
@@ -59,9 +139,23 @@ class ScanossGrpc(ScanossBase):
59
139
  Client for gRPC functionality
60
140
  """
61
141
 
62
- def __init__(self, url: str = None, debug: bool = False, trace: bool = False, quiet: bool = False,
63
- ca_cert: str = None, api_key: str = None, ver_details: str = None, timeout: int = 600,
64
- proxy: str = None, grpc_proxy: str = None, pac: PACFile = None):
142
+ def __init__( # noqa: PLR0912, PLR0913, PLR0915
143
+ self,
144
+ url: Optional[str] = None,
145
+ debug: bool = False,
146
+ trace: bool = False,
147
+ quiet: bool = False,
148
+ ca_cert: Optional[str] = None,
149
+ api_key: Optional[str] = None,
150
+ ver_details: Optional[str] = None,
151
+ timeout: int = 600,
152
+ proxy: Optional[str] = None,
153
+ grpc_proxy: Optional[str] = None,
154
+ pac: Optional[PACFile] = None,
155
+ req_headers: Optional[dict] = None,
156
+ ignore_cert_errors: bool = False,
157
+ use_grpc: Optional[bool] = False,
158
+ ):
65
159
  """
66
160
 
67
161
  :param url:
@@ -78,22 +172,56 @@ class ScanossGrpc(ScanossBase):
78
172
  grpc_proxy='http://<ip>:<port>'
79
173
  """
80
174
  super().__init__(debug, trace, quiet)
81
- self.url = url if url else SCANOSS_GRPC_URL
82
175
  self.api_key = api_key if api_key else SCANOSS_API_KEY
83
- if self.api_key and not url and not os.environ.get("SCANOSS_GRPC_URL"):
84
- self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium
85
- self.url = self.url.lower()
86
- self.orig_url = self.url # Used for proxy lookup
87
176
  self.timeout = timeout
88
177
  self.proxy = proxy
89
178
  self.grpc_proxy = grpc_proxy
90
179
  self.pac = pac
91
180
  self.metadata = []
181
+ self.ignore_cert_errors = ignore_cert_errors
182
+ self.use_grpc = use_grpc
183
+ self.req_headers = req_headers if req_headers else {}
184
+ self.headers = {}
185
+ self.retry_limit = 2 # default retry limit
186
+
92
187
  if self.api_key:
93
188
  self.metadata.append(('x-api-key', api_key)) # Set API key if we have one
189
+ self.headers['X-Session'] = self.api_key
190
+ self.headers['x-api-key'] = self.api_key
94
191
  if ver_details:
95
192
  self.metadata.append(('x-scanoss-client', ver_details))
96
- self.metadata.append(('user-agent', f'scanoss-py/{__version__}'))
193
+ self.headers['x-scanoss-client'] = ver_details
194
+ user_agent = f'scanoss-py/{__version__}'
195
+ self.metadata.append(('user-agent', user_agent))
196
+ self.headers['User-Agent'] = user_agent
197
+ self.headers['user-agent'] = user_agent
198
+ self.headers['Content-Type'] = 'application/json'
199
+ # Set the correct URL/API key combination
200
+ self.url = url if url else SCANOSS_GRPC_URL
201
+ if self.api_key and not url and not os.environ.get('SCANOSS_GRPC_URL'):
202
+ self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium
203
+ self.load_generic_headers(url)
204
+ self.url = self.url.lower()
205
+ self.orig_url = self.url.strip().rstrip('/') # Used for proxy lookup
206
+ # REST setup
207
+ if self.trace:
208
+ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
209
+ http_client.HTTPConnection.debuglevel = 1
210
+ if pac and not proxy: # Set up a PAC session if requested (and no proxy has been explicitly set)
211
+ self.print_debug('Setting up PAC session...')
212
+ self.session = PACSession(pac=pac)
213
+ else:
214
+ self.session = requests.sessions.Session()
215
+ if self.ignore_cert_errors:
216
+ self.print_debug('Ignoring cert errors...')
217
+ urllib3.disable_warnings(InsecureRequestWarning)
218
+ self.session.verify = False
219
+ elif ca_cert:
220
+ self.session.verify = ca_cert
221
+ self.proxies = {'https': proxy, 'http': proxy} if proxy else None
222
+ if self.proxies:
223
+ self.session.proxies = self.proxies
224
+
97
225
  secure = True if self.url.startswith('https:') else False # Is it a secure connection?
98
226
  if self.url.startswith('http'):
99
227
  u = urlparse(self.url)
@@ -107,12 +235,15 @@ class ScanossGrpc(ScanossBase):
107
235
  cert_data = ScanossGrpc._load_cert(ca_cert)
108
236
  self.print_debug(f'Setting up (secure: {secure}) connection to {self.url}...')
109
237
  self._get_proxy_config()
110
- if secure is False: # insecure connection
238
+ if not secure: # insecure connection
111
239
  self.comp_search_stub = ComponentsStub(grpc.insecure_channel(self.url))
112
240
  self.crypto_stub = CryptographyStub(grpc.insecure_channel(self.url))
113
241
  self.dependencies_stub = DependenciesStub(grpc.insecure_channel(self.url))
114
242
  self.semgrep_stub = SemgrepStub(grpc.insecure_channel(self.url))
115
243
  self.vuln_stub = VulnerabilitiesStub(grpc.insecure_channel(self.url))
244
+ self.provenance_stub = GeoProvenanceStub(grpc.insecure_channel(self.url))
245
+ self.scanning_stub = ScanningStub(grpc.insecure_channel(self.url))
246
+ self.license_stub = LicenseStub(grpc.insecure_channel(self.url))
116
247
  else:
117
248
  if ca_cert is not None:
118
249
  credentials = grpc.ssl_channel_credentials(cert_data) # secure with specified certificate
@@ -123,68 +254,34 @@ class ScanossGrpc(ScanossBase):
123
254
  self.dependencies_stub = DependenciesStub(grpc.secure_channel(self.url, credentials))
124
255
  self.semgrep_stub = SemgrepStub(grpc.secure_channel(self.url, credentials))
125
256
  self.vuln_stub = VulnerabilitiesStub(grpc.secure_channel(self.url, credentials))
257
+ self.provenance_stub = GeoProvenanceStub(grpc.secure_channel(self.url, credentials))
258
+ self.scanning_stub = ScanningStub(grpc.secure_channel(self.url, credentials))
259
+ self.license_stub = LicenseStub(grpc.secure_channel(self.url, credentials))
126
260
 
127
261
  @classmethod
128
262
  def _load_cert(cls, cert_file: str) -> bytes:
129
263
  with open(cert_file, 'rb') as f:
130
264
  return f.read()
131
265
 
132
- def deps_echo(self, message: str = 'Hello there!') -> str:
266
+ def deps_echo(self, message: str = 'Hello there!') -> Optional[dict]:
133
267
  """
134
268
  Send Echo message to the Dependency service
135
269
  :param self:
136
270
  :param message: Message to send (default: Hello there!)
137
271
  :return: echo or None
138
272
  """
139
- request_id = str(uuid.uuid4())
140
- resp: EchoResponse
141
- try:
142
- metadata = self.metadata[:]
143
- metadata.append(('x-request-id', request_id)) # Set a Request ID
144
- resp = self.dependencies_stub.Echo(EchoRequest(message=message), metadata=metadata, timeout=3)
145
- except Exception as e:
146
- self.print_stderr(f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message '
147
- f'(rqId: {request_id}): {e}')
148
- else:
149
- # self.print_stderr(f'resp: {resp} - call: {call}')
150
- # response_id = ""
151
- # if not call:
152
- # self.print_stderr(f'No call to leverage.')
153
- # for key, value in call.trailing_metadata():
154
- # print('Greeter client received trailing metadata: key=%s value=%s' % (key, value))
155
- #
156
- # for key, value in call.trailing_metadata():
157
- # if key == 'x-response-id':
158
- # response_id = value
159
- # self.print_stderr(f'Response ID: {response_id}. Metadata: {call.trailing_metadata()}')
160
- if resp:
161
- return resp.message
162
- self.print_stderr(f'ERROR: Problem sending Echo request ({message}) to {self.url}. rqId: {request_id}')
163
- return None
273
+ return self._call_api('dependencies.Echo', self.dependencies_stub.Echo, {'message': message}, EchoRequest)
164
274
 
165
- def crypto_echo(self, message: str = 'Hello there!') -> str:
275
+ def crypto_echo(self, message: str = 'Hello there!') -> Optional[dict]:
166
276
  """
167
277
  Send Echo message to the Cryptography service
168
278
  :param self:
169
279
  :param message: Message to send (default: Hello there!)
170
280
  :return: echo or None
171
281
  """
172
- request_id = str(uuid.uuid4())
173
- resp: EchoResponse
174
- try:
175
- metadata = self.metadata[:]
176
- metadata.append(('x-request-id', request_id)) # Set a Request ID
177
- resp = self.crypto_stub.Echo(EchoRequest(message=message), metadata=metadata, timeout=3)
178
- except Exception as e:
179
- self.print_stderr(f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message '
180
- f'(rqId: {request_id}): {e}')
181
- else:
182
- if resp:
183
- return resp.message
184
- self.print_stderr(f'ERROR: Problem sending Echo request ({message}) to {self.url}. rqId: {request_id}')
185
- return None
282
+ return self._call_api('cryptography.Echo', self.crypto_stub.Echo, {'message': message}, EchoRequest)
186
283
 
187
- def get_dependencies(self, dependencies: json, depth: int = 1) -> dict:
284
+ def get_dependencies(self, dependencies: Optional[dict] = None, depth: int = 1) -> Optional[dict]:
188
285
  if not dependencies:
189
286
  self.print_stderr('ERROR: No dependency data supplied to submit to the API.')
190
287
  return None
@@ -193,7 +290,7 @@ class ScanossGrpc(ScanossBase):
193
290
  self.print_stderr(f'ERROR: No response for dependency request: {dependencies}')
194
291
  return resp
195
292
 
196
- def get_dependencies_json(self, dependencies: dict, depth: int = 1) -> dict:
293
+ def get_dependencies_json(self, dependencies: dict, depth: int = 1) -> Optional[dict]:
197
294
  """
198
295
  Client function to call the rpc for GetDependencies
199
296
  :param dependencies: Message to send to the service
@@ -201,197 +298,293 @@ class ScanossGrpc(ScanossBase):
201
298
  :return: Server response or None
202
299
  """
203
300
  if not dependencies:
204
- self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
301
+ self.print_stderr('ERROR: No message supplied to send to gRPC service.')
205
302
  return None
206
- request_id = str(uuid.uuid4())
207
- resp: DependencyResponse
208
- try:
209
- files_json = dependencies.get("files")
210
- if files_json is None or len(files_json) == 0:
211
- self.print_stderr(f'ERROR: No dependency data supplied to send to gRPC service.')
212
- return None
213
- request = ParseDict(dependencies, DependencyRequest()) # Parse the JSON/Dict into the dependency object
214
- request.depth = depth
215
- metadata = self.metadata[:]
216
- metadata.append(('x-request-id', request_id)) # Set a Request ID
217
- self.print_debug(f'Sending dependency data for decoration (rqId: {request_id})...')
218
- resp = self.dependencies_stub.GetDependencies(request, metadata=metadata, timeout=self.timeout)
219
- except Exception as e:
220
- self.print_stderr(f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message '
221
- f'(rqId: {request_id}): {e}')
222
- else:
223
- if resp:
224
- if not self._check_status_response(resp.status, request_id):
225
- return None
226
- return MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dictionary
227
- return None
303
+ files_json = dependencies.get('files')
304
+ if files_json is None or len(files_json) == 0:
305
+ self.print_stderr('ERROR: No dependency data supplied to send to decoration service.')
306
+ return None
307
+ all_responses = []
308
+ # Process the dependency files in parallel
309
+ with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_CONCURRENT_REQUESTS) as executor:
310
+ future_to_file = {
311
+ executor.submit(self._process_dep_file, file, depth, self.use_grpc): file for file in files_json
312
+ }
313
+ for future in concurrent.futures.as_completed(future_to_file):
314
+ response = future.result()
315
+ if response:
316
+ all_responses.append(response)
317
+ # End of concurrent processing
318
+ success_status = 'SUCCESS'
319
+ merged_response = {'files': [], 'status': {'status': success_status, 'message': 'Success'}}
320
+ # Merge the responses
321
+ for response in all_responses:
322
+ if response:
323
+ if 'files' in response and len(response['files']) > 0:
324
+ merged_response['files'].append(response['files'][0])
325
+ # Overwrite the status if any of the responses was not successful
326
+ if 'status' in response and response['status']['status'] != success_status:
327
+ merged_response['status'] = response['status']
328
+ return merged_response
228
329
 
229
- def get_crypto_json(self, purls: dict) -> dict:
330
+ def _process_dep_file(self, file, depth: int = 1, use_grpc: Optional[bool] = None) -> Optional[dict]:
230
331
  """
231
- Client function to call the rpc for Cryptography GetAlgorithms
232
- :param purls: Message to send to the service
233
- :return: Server response or None
332
+ Process a single dependency file using either gRPC or REST
333
+
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)
338
+
339
+ Returns:
340
+ response JSON or None
234
341
  """
235
- if not purls:
236
- self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
237
- return None
238
- request_id = str(uuid.uuid4())
239
- resp: AlgorithmResponse
240
- try:
241
- request = ParseDict(purls, PurlRequest()) # Parse the JSON/Dict into the purl request object
242
- metadata = self.metadata[:]
243
- metadata.append(('x-request-id', request_id)) # Set a Request ID
244
- self.print_debug(f'Sending crypto data for decoration (rqId: {request_id})...')
245
- resp = self.crypto_stub.GetAlgorithms(request, metadata=metadata, timeout=self.timeout)
246
- except Exception as e:
247
- self.print_stderr(f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message '
248
- f'(rqId: {request_id}): {e}')
249
- else:
250
- if resp:
251
- if not self._check_status_response(resp.status, request_id):
252
- return None
253
- resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
254
- del resp_dict['status']
255
- return resp_dict
256
- return None
342
+ file_request = {'files': [file], 'depth': depth}
343
+
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
+ )
257
352
 
258
- def get_vulnerabilities_json(self, purls: dict) -> dict:
353
+ def get_vulnerabilities_json(self, purls: Optional[dict] = None, use_grpc: Optional[bool] = None) -> Optional[dict]:
259
354
  """
260
355
  Client function to call the rpc for Vulnerability GetVulnerabilities
261
- :param purls: Message to send to the service
262
- :return: Server response or 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
263
363
  """
264
- if not purls:
265
- self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
266
- return None
267
- request_id = str(uuid.uuid4())
268
- resp: VulnerabilityResponse
269
- try:
270
- request = ParseDict(purls, PurlRequest()) # Parse the JSON/Dict into the purl request object
271
- metadata = self.metadata[:]
272
- metadata.append(('x-request-id', request_id)) # Set a Request ID
273
- self.print_debug(f'Sending crypto data for decoration (rqId: {request_id})...')
274
- resp = self.vuln_stub.GetVulnerabilities(request, metadata=metadata, timeout=self.timeout)
275
- except Exception as e:
276
- self.print_stderr(f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message '
277
- f'(rqId: {request_id}): {e}')
278
- else:
279
- if resp:
280
- if not self._check_status_response(resp.status, request_id):
281
- return None
282
- resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
283
- del resp_dict['status']
284
- return resp_dict
285
- return None
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
+ )
286
372
 
287
- 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]:
288
374
  """
289
375
  Client function to call the rpc for Semgrep GetIssues
290
- :param purls: Message to send to the service
291
- :return: Server response or None
376
+
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
292
383
  """
293
- if not purls:
294
- self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
295
- return None
296
- request_id = str(uuid.uuid4())
297
- resp: SemgrepResponse
298
- try:
299
- request = ParseDict(purls, PurlRequest()) # Parse the JSON/Dict into the purl request object
300
- metadata = self.metadata[:]
301
- metadata.append(('x-request-id', request_id)) # Set a Request ID
302
- self.print_debug(f'Sending semgrep data for decoration (rqId: {request_id})...')
303
- resp = self.semgrep_stub.GetIssues(request, metadata=metadata, timeout=self.timeout)
304
- except Exception as e:
305
- self.print_stderr(f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message '
306
- f'(rqId: {request_id}): {e}')
307
- else:
308
- if resp:
309
- if not self._check_status_response(resp.status, request_id):
310
- return None
311
- resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
312
- del resp_dict['status']
313
- return resp_dict
314
- return None
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
+ )
315
392
 
316
- def search_components_json(self, search: dict) -> dict:
393
+ def search_components_json(self, search: dict, use_grpc: Optional[bool] = None) -> Optional[dict]:
317
394
  """
318
395
  Client function to call the rpc for Components SearchComponents
319
- :param search: Message to send to the service
320
- :return: Server response or None
396
+
397
+ Args:
398
+ search (dict): Message to send to the service
399
+ Returns:
400
+ Server response or None
321
401
  """
322
- if not search:
323
- self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
324
- return None
325
- request_id = str(uuid.uuid4())
326
- resp: CompSearchResponse
327
- try:
328
- request = ParseDict(search, CompSearchRequest()) # Parse the JSON/Dict into the purl request object
329
- metadata = self.metadata[:]
330
- metadata.append(('x-request-id', request_id)) # Set a Request ID
331
- self.print_debug(f'Sending component search data (rqId: {request_id})...')
332
- resp = self.comp_search_stub.SearchComponents(request, metadata=metadata, timeout=self.timeout)
333
- except Exception as e:
334
- self.print_stderr(f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message '
335
- f'(rqId: {request_id}): {e}')
336
- else:
337
- if resp:
338
- if not self._check_status_response(resp.status, request_id):
339
- return None
340
- resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
341
- del resp_dict['status']
342
- return resp_dict
343
- return None
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
+ )
344
410
 
345
- def get_component_versions_json(self, search: dict) -> dict:
411
+ def get_component_versions_json(self, search: dict, use_grpc: Optional[bool] = None) -> Optional[dict]:
346
412
  """
347
413
  Client function to call the rpc for Components GetComponentVersions
348
- :param search: Message to send to the service
349
- :return: Server response or 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
+ )
428
+
429
+ def folder_hash_scan(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
430
+ """
431
+ Client function to call the rpc for Folder Hashing Scan
432
+
433
+ Args:
434
+ request (Dict): Folder Hash Request
435
+ use_grpc (Optional[bool]): Whether to use gRPC or REST API
436
+
437
+ Returns:
438
+ Optional[Dict]: Folder Hash Response, or None if the request was not succesfull
350
439
  """
351
- if not search:
352
- self.print_stderr(f'ERROR: No message supplied to send to gRPC service.')
440
+ return self._call_api(
441
+ 'scanning.FolderHashScan',
442
+ self.scanning_stub.FolderHashScan,
443
+ request,
444
+ HFHRequest,
445
+ 'Sending folder hash scan data (rqId: {rqId})...',
446
+ use_grpc=use_grpc,
447
+ )
448
+
449
+ def _call_api(
450
+ self,
451
+ endpoint_key: str,
452
+ rpc_method,
453
+ request_input,
454
+ request_type,
455
+ debug_msg: Optional[str] = None,
456
+ use_grpc: Optional[bool] = None,
457
+ ) -> Optional[Dict]:
458
+ """
459
+ Unified method to call either gRPC or REST API based on configuration
460
+
461
+ Args:
462
+ endpoint_key (str): The key to lookup the REST endpoint in REST_ENDPOINTS
463
+ rpc_method: The gRPC stub method (used only if use_grpc is True)
464
+ request_input: Either a dict or a gRPC request object
465
+ request_type: The type of the gRPC request object (used only if use_grpc is True)
466
+ debug_msg (str, optional): Debug message template that can include {rqId} placeholder
467
+ use_grpc (bool, optional): Override the instance's use_grpc setting. If None, uses self.use_grpc
468
+
469
+ Returns:
470
+ dict: The parsed response as a dictionary, or None if something went wrong
471
+ """
472
+ if not request_input:
473
+ self.print_stderr('ERROR: No message supplied to send to service.')
353
474
  return None
475
+
476
+ # Determine whether to use gRPC or REST
477
+ use_grpc_flag = use_grpc if use_grpc is not None else self.use_grpc
478
+
479
+ if use_grpc_flag:
480
+ return self._call_rpc(rpc_method, request_input, request_type, debug_msg)
481
+ else:
482
+ # For REST, we only need the dict input
483
+ if not isinstance(request_input, dict):
484
+ request_input = MessageToDict(request_input, preserving_proto_field_name=True)
485
+ return self._call_rest(endpoint_key, request_input, debug_msg)
486
+
487
+ def _call_rpc(self, rpc_method, request_input, request_type, debug_msg: Optional[str] = None) -> Optional[Dict]:
488
+ """
489
+ Call a gRPC method and return the response as a dictionary
490
+
491
+ Args:
492
+ rpc_method (): The gRPC stub method
493
+ request_input (): Either a dict or a gRPC request object.
494
+ request_type (): The type of the gRPC request object.
495
+ debug_msg (str, optional): Debug message template that can include {rqId} placeholder.
496
+
497
+ Returns:
498
+ dict: The parsed gRPC response as a dictionary, or None if something went wrong
499
+ """
354
500
  request_id = str(uuid.uuid4())
355
- resp: CompVersionResponse
356
- try:
357
- request = ParseDict(search, CompVersionRequest()) # Parse the JSON/Dict into the purl request object
358
- metadata = self.metadata[:]
359
- metadata.append(('x-request-id', request_id)) # Set a Request ID
360
- self.print_debug(f'Sending component version data (rqId: {request_id})...')
361
- resp = self.comp_search_stub.GetComponentVersions(request, metadata=metadata, timeout=self.timeout)
362
- except Exception as e:
363
- self.print_stderr(f'ERROR: {e.__class__.__name__} Problem encountered sending gRPC message '
364
- f'(rqId: {request_id}): {e}')
501
+ if isinstance(request_input, dict):
502
+ request_obj = ParseDict(request_input, request_type())
365
503
  else:
366
- if resp:
367
- if not self._check_status_response(resp.status, request_id):
368
- return None
369
- resp_dict = MessageToDict(resp, preserving_proto_field_name=True) # Convert gRPC response to a dict
370
- del resp_dict['status']
371
- return resp_dict
372
- return None
504
+ request_obj = request_input
505
+ metadata = self.metadata[:] + [('x-request-id', request_id)]
506
+ if debug_msg:
507
+ self.print_debug(debug_msg.format(rqId=request_id))
508
+ try:
509
+ resp = rpc_method(request_obj, metadata=metadata, timeout=self.timeout)
510
+ except grpc.RpcError as e:
511
+ raise ScanossGrpcError(
512
+ f'{e.__class__.__name__} while sending gRPC message (rqId: {request_id}): {e.details()}'
513
+ )
514
+ if resp and not self._check_status_response_grpc(resp.status, request_id):
515
+ return None
516
+
517
+ resp_dict = MessageToDict(resp, preserving_proto_field_name=True)
518
+ return resp_dict
373
519
 
374
- def _check_status_response(self, status_response: StatusResponse, request_id: str = None) -> bool:
520
+ def _check_status_response_grpc(self, status_response: StatusResponse, request_id: str = None) -> bool:
375
521
  """
376
522
  Check the response object to see if the command was successful or not
377
523
  :param status_response: Status Response
378
524
  :return: True if successful, False otherwise
379
525
  """
526
+
380
527
  if not status_response:
381
528
  self.print_stderr(f'Warning: No status response supplied (rqId: {request_id}). Assuming it was ok.')
382
529
  return True
383
530
  self.print_debug(f'Checking response status (rqId: {request_id}): {status_response}')
384
531
  status_code: StatusCode = status_response.status
385
- if status_code > 1:
386
- msg = "Unsuccessful"
387
- if status_code == 2:
388
- msg = "Succeeded with warnings"
389
- elif status_code == 3:
390
- msg = "Failed with warnings"
532
+ if status_code > ScanossGrpcStatusCode.SUCCESS:
533
+ ret_val = False # default to failed
534
+ msg = 'Unsuccessful'
535
+ if status_code == ScanossGrpcStatusCode.SUCCEEDED_WITH_WARNINGS:
536
+ msg = 'Succeeded with warnings'
537
+ ret_val = True # No need to fail as it succeeded with warnings
538
+ elif status_code == ScanossGrpcStatusCode.WARNING:
539
+ msg = 'Failed with warnings'
391
540
  self.print_stderr(f'{msg} (rqId: {request_id} - status: {status_code}): {status_response.message}')
392
- return False
541
+ return ret_val
393
542
  return True
394
543
 
544
+ def check_status_response_rest(self, status_dict: dict, request_id: Optional[str] = None) -> bool:
545
+ """
546
+ Check the REST response dictionary to see if the command was successful or not
547
+
548
+ Args:
549
+ status_dict (dict): Status dictionary from REST response containing 'status' and 'message' keys
550
+ request_id (str, optional): Request ID for logging
551
+ Returns:
552
+ bool: True if successful, False otherwise
553
+ """
554
+ if not status_dict:
555
+ self.print_stderr(f'Warning: No status response supplied (rqId: {request_id}). Assuming it was ok.')
556
+ return True
557
+
558
+ if request_id:
559
+ self.print_debug(f'Checking response status (rqId: {request_id}): {status_dict}')
560
+
561
+ # Get status from dictionary - it can be either a string or nested dict
562
+ status = status_dict.get('status')
563
+ message = status_dict.get('message', '')
564
+ ret_val = True
565
+
566
+ # Handle case where status might be a string directly
567
+ if isinstance(status, str):
568
+ status_str = status.upper()
569
+ if status_str == ScanossRESTStatusCode.SUCCESS.value:
570
+ ret_val = True
571
+ elif status_str == ScanossRESTStatusCode.SUCCEEDED_WITH_WARNINGS.value:
572
+ self.print_stderr(f'Succeeded with warnings (rqId: {request_id}): {message}')
573
+ ret_val = True
574
+ elif status_str == ScanossRESTStatusCode.WARNING.value:
575
+ self.print_stderr(f'Failed with warnings (rqId: {request_id}): {message}')
576
+ ret_val = False
577
+ elif status_str == ScanossRESTStatusCode.FAILED.value:
578
+ self.print_stderr(f'Unsuccessful (rqId: {request_id}): {message}')
579
+ ret_val = False
580
+ else:
581
+ self.print_debug(f'Unknown status "{status_str}" (rqId: {request_id}). Assuming success.')
582
+ ret_val = True
583
+
584
+ # Otherwise asume success
585
+ self.print_debug(f'Unexpected status type {type(status)} (rqId: {request_id}). Assuming success.')
586
+ return ret_val
587
+
395
588
  def _get_proxy_config(self):
396
589
  """
397
590
  Set the grpc_proxy/http_proxy/https_proxy environment variables if PAC file has been specified
@@ -399,21 +592,376 @@ class ScanossGrpc(ScanossBase):
399
592
  :param self:
400
593
  """
401
594
  if self.grpc_proxy:
402
- self.print_debug(f'Setting GRPC (grpc_proxy) proxy...')
403
- os.environ["grpc_proxy"] = self.grpc_proxy
595
+ self.print_debug('Setting GRPC (grpc_proxy) proxy...')
596
+ os.environ['grpc_proxy'] = self.grpc_proxy
404
597
  elif self.proxy:
405
- self.print_debug(f'Setting GRPC (http_proxy/https_proxy) proxies...')
406
- os.environ["http_proxy"] = self.proxy
407
- os.environ["https_proxy"] = self.proxy
598
+ self.print_debug('Setting GRPC (http_proxy/https_proxy) proxies...')
599
+ os.environ['http_proxy'] = self.proxy
600
+ os.environ['https_proxy'] = self.proxy
408
601
  elif self.pac:
409
602
  self.print_debug(f'Attempting to get GRPC proxy details from PAC for {self.orig_url}...')
410
603
  resolver = ProxyResolver(self.pac)
411
604
  proxies = resolver.get_proxy_for_requests(self.orig_url)
412
605
  if proxies:
413
606
  self.print_trace(f'Setting proxies: {proxies}')
414
- os.environ["http_proxy"] = proxies.get("http") or ""
415
- os.environ["https_proxy"] = proxies.get("https") or ""
607
+ os.environ['http_proxy'] = proxies.get('http') or ''
608
+ os.environ['https_proxy'] = proxies.get('https') or ''
609
+
610
+ def get_provenance_json(self, purls: dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
611
+ """
612
+ Client function to call the rpc for GetComponentContributors
613
+
614
+ Args:
615
+ purls (dict): ComponentsRequest
616
+ use_grpc (bool): Whether to use gRPC or REST (None = use instance default)
617
+
618
+ Returns:
619
+ dict: JSON response or None
620
+ """
621
+ return self._call_api(
622
+ 'geoprovenance.GetCountryContributorsByComponents',
623
+ self.provenance_stub.GetCountryContributorsByComponents,
624
+ purls,
625
+ ComponentsRequest,
626
+ 'Sending data for provenance decoration (rqId: {rqId})...',
627
+ use_grpc=use_grpc,
628
+ )
629
+
630
+ def get_provenance_origin(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
631
+ """
632
+ Client function to call the rpc for GetOriginByComponents
633
+
634
+ Args:
635
+ request (Dict): GetOriginByComponents Request
636
+
637
+ Returns:
638
+ Optional[Dict]: OriginResponse, or None if the request was not successfull
639
+ """
640
+ return self._call_api(
641
+ 'geoprovenance.GetOriginByComponents',
642
+ self.provenance_stub.GetOriginByComponents,
643
+ request,
644
+ ComponentsRequest,
645
+ 'Sending data for provenance origin decoration (rqId: {rqId})...',
646
+ use_grpc=use_grpc,
647
+ )
648
+
649
+ def get_crypto_algorithms_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
650
+ """
651
+ Client function to call the rpc for GetComponentsAlgorithms for a list of purls
652
+
653
+ Args:
654
+ request (Dict): ComponentsRequest
655
+ use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default)
656
+
657
+ Returns:
658
+ Optional[Dict]: AlgorithmResponse, or None if the request was not successfull
659
+ """
660
+ return self._call_api(
661
+ 'cryptography.GetComponentsAlgorithms',
662
+ self.crypto_stub.GetComponentsAlgorithms,
663
+ request,
664
+ ComponentsRequest,
665
+ 'Sending data for cryptographic algorithms decoration (rqId: {rqId})...',
666
+ use_grpc=use_grpc,
667
+ )
668
+
669
+ def get_crypto_algorithms_in_range_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
670
+ """
671
+ Client function to call the rpc for GetComponentsAlgorithmsInRange for a list of purls
672
+
673
+ Args:
674
+ request (Dict): ComponentsRequest
675
+ use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default)
676
+
677
+ Returns:
678
+ Optional[Dict]: AlgorithmsInRangeResponse, or None if the request was not successfull
679
+ """
680
+ return self._call_api(
681
+ 'cryptography.GetComponentsAlgorithmsInRange',
682
+ self.crypto_stub.GetComponentsAlgorithmsInRange,
683
+ request,
684
+ ComponentsRequest,
685
+ 'Sending data for cryptographic algorithms in range decoration (rqId: {rqId})...',
686
+ use_grpc=use_grpc,
687
+ )
688
+
689
+ def get_encryption_hints_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
690
+ """
691
+ Client function to call the rpc for GetComponentsEncryptionHints for a list of purls
692
+
693
+ Args:
694
+ request (Dict): ComponentsRequest
695
+ use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default)
696
+
697
+ Returns:
698
+ Optional[Dict]: HintsResponse, or None if the request was not successfull
699
+ """
700
+ return self._call_api(
701
+ 'cryptography.GetComponentsEncryptionHints',
702
+ self.crypto_stub.GetComponentsEncryptionHints,
703
+ request,
704
+ ComponentsRequest,
705
+ 'Sending data for encryption hints decoration (rqId: {rqId})...',
706
+ use_grpc=use_grpc,
707
+ )
708
+
709
+ def get_encryption_hints_in_range_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
710
+ """
711
+ Client function to call the rpc for GetComponentsHintsInRange for a list of purls
712
+
713
+ Args:
714
+ request (Dict): ComponentsRequest
715
+ use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default)
716
+
717
+ Returns:
718
+ Optional[Dict]: HintsInRangeResponse, or None if the request was not successfull
719
+ """
720
+ return self._call_api(
721
+ 'cryptography.GetComponentsHintsInRange',
722
+ self.crypto_stub.GetComponentsHintsInRange,
723
+ request,
724
+ ComponentsRequest,
725
+ 'Sending data for encryption hints in range decoration (rqId: {rqId})...',
726
+ use_grpc=use_grpc,
727
+ )
728
+
729
+ def get_versions_in_range_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
730
+ """
731
+ Client function to call the rpc for GetComponentsVersionsInRange for a list of purls
732
+
733
+ Args:
734
+ request (Dict): ComponentsRequest
735
+ use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default)
736
+
737
+ Returns:
738
+ Optional[Dict]: VersionsInRangeResponse, or None if the request was not successfull
739
+ """
740
+ return self._call_api(
741
+ 'cryptography.GetComponentsVersionsInRange',
742
+ self.crypto_stub.GetComponentsVersionsInRange,
743
+ request,
744
+ ComponentsRequest,
745
+ 'Sending data for cryptographic versions in range decoration (rqId: {rqId})...',
746
+ use_grpc=use_grpc,
747
+ )
748
+
749
+ def get_licenses(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]:
750
+ """
751
+ Client function to call the rpc for Licenses GetComponentsLicenses
752
+ It will either use REST (default) or gRPC depending on the use_grpc flag
753
+
754
+ Args:
755
+ request (Dict): ComponentsRequest
756
+ Returns:
757
+ Optional[Dict]: ComponentsLicenseResponse, or None if the request was not successfull
758
+ """
759
+ return self._call_api(
760
+ 'licenses.GetComponentsLicenses',
761
+ self.license_stub.GetComponentsLicenses,
762
+ request,
763
+ ComponentsRequest,
764
+ 'Sending data for license decoration (rqId: {rqId})...',
765
+ use_grpc=use_grpc,
766
+ )
767
+
768
+ def load_generic_headers(self, url: Optional[str] = None):
769
+ """
770
+ Adds custom headers from req_headers to metadata.
771
+
772
+ If x-api-key is present and no URL is configured (directly or via
773
+ environment), sets URL to the premium endpoint (DEFAULT_URL2).
774
+ """
775
+ if self.req_headers: # Load generic headers
776
+ for key, value in self.req_headers.items():
777
+ if key == 'x-api-key': # Set premium URL if x-api-key header is set
778
+ if not url and not os.environ.get('SCANOSS_GRPC_URL'):
779
+ self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium
780
+ self.api_key = value
781
+ self.metadata.append((key, value))
782
+ self.headers[key] = value
783
+
784
+ #
785
+ # End of gRPC Client Functions
786
+ #
787
+ # Start of REST Client Functions
788
+ #
789
+
790
+ def _rest_get(self, uri: str, request_id: str, params: Optional[dict] = None) -> Optional[dict]:
791
+ """
792
+ Send a GET request to the specified URI with optional query parameters.
793
+
794
+ Args:
795
+ uri (str): URI to send GET request to
796
+ request_id (str): request id
797
+ params (dict, optional): Optional query parameters as dictionary
798
+
799
+ Returns:
800
+ dict: JSON response or None
801
+ """
802
+ if not uri:
803
+ self.print_stderr('Error: Missing URI. Cannot perform GET request.')
804
+ return None
805
+ self.print_trace(f'Sending REST GET request to {uri}...')
806
+ headers = self.headers.copy()
807
+ headers['x-request-id'] = request_id
808
+ retry = 0
809
+ while retry <= self.retry_limit:
810
+ retry += 1
811
+ try:
812
+ response = self.session.get(uri, headers=headers, params=params, timeout=self.timeout)
813
+ response.raise_for_status() # Raises an HTTPError for bad responses
814
+ return response.json()
815
+ except (requests.exceptions.SSLError, requests.exceptions.ProxyError) as e:
816
+ self.print_stderr(f'ERROR: Exception ({e.__class__.__name__}) sending GET request - {e}.')
817
+ raise Exception(f'ERROR: The SCANOSS API GET request failed for {uri}') from e
818
+ except requests.exceptions.HTTPError as e:
819
+ self.print_stderr(f'ERROR: HTTP error sending GET request ({request_id}): {e}')
820
+ raise Exception(
821
+ f'ERROR: The SCANOSS API GET request failed with status {e.response.status_code} for {uri}'
822
+ ) from e
823
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
824
+ if retry > self.retry_limit: # Timed out retry_limit or more times, fail
825
+ self.print_stderr(f'ERROR: {e.__class__.__name__} sending GET request ({request_id}): {e}')
826
+ raise Exception(
827
+ f'ERROR: The SCANOSS API GET request timed out ({e.__class__.__name__}) for {uri}'
828
+ ) from e
829
+ else:
830
+ self.print_stderr(f'Warning: {e.__class__.__name__} communicating with {self.url}. Retrying...')
831
+ time.sleep(5)
832
+ except requests.exceptions.RequestException as e:
833
+ self.print_stderr(f'Error: Problem sending GET request to {uri}: {e}')
834
+ raise Exception(f'ERROR: The SCANOSS API GET request failed for {uri}') from e
835
+ except Exception as e:
836
+ self.print_stderr(
837
+ f'ERROR: Exception ({e.__class__.__name__}) sending GET request ({request_id}) to {uri}: {e}'
838
+ )
839
+ raise Exception(f'ERROR: The SCANOSS API GET request failed for {uri}') from e
840
+ return None
841
+
842
+ def _rest_post(self, uri: str, request_id: str, data: dict) -> Optional[dict]:
843
+ """
844
+ Post the specified data to the given URI.
845
+
846
+ Args:
847
+ uri (str): URI to post to
848
+ request_id (str): request id
849
+ data (dict): json data to post
850
+
851
+ Returns:
852
+ dict: JSON response or None
853
+ """
854
+ if not uri:
855
+ self.print_stderr('Error: Missing URI. Cannot search for project.')
856
+ return None
857
+ self.print_trace(f'Sending REST POST data to {uri}...')
858
+ headers = self.headers.copy()
859
+ headers['x-request-id'] = request_id
860
+ retry = 0
861
+ while retry <= self.retry_limit:
862
+ retry += 1
863
+ try:
864
+ response = self.session.post(uri, headers=headers, json=data, timeout=self.timeout)
865
+ response.raise_for_status() # Raises an HTTPError for bad responses
866
+ return response.json()
867
+ except (requests.exceptions.SSLError, requests.exceptions.ProxyError) as e:
868
+ self.print_stderr(f'ERROR: Exception ({e.__class__.__name__}) POSTing data - {e}.')
869
+ raise Exception(f'ERROR: The SCANOSS Decoration API request failed for {uri}') from e
870
+ except requests.exceptions.HTTPError as e:
871
+ self.print_stderr(f'ERROR: HTTP error POSTing data ({request_id}): {e}')
872
+ raise Exception(
873
+ f'ERROR: The SCANOSS Decoration API request failed with status {e.response.status_code} for {uri}'
874
+ ) from e
875
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
876
+ if retry > self.retry_limit: # Timed out retry_limit or more times, fail
877
+ self.print_stderr(f'ERROR: {e.__class__.__name__} POSTing decoration data ({request_id}): {e}')
878
+ raise Exception(
879
+ f'ERROR: The SCANOSS Decoration API request timed out ({e.__class__.__name__}) for {uri}'
880
+ ) from e
881
+ else:
882
+ self.print_stderr(f'Warning: {e.__class__.__name__} communicating with {self.url}. Retrying...')
883
+ time.sleep(5)
884
+ except requests.exceptions.RequestException as e:
885
+ self.print_stderr(f'Error: Problem posting data to {uri}: {e}')
886
+ raise Exception(f'ERROR: The SCANOSS Decoration API request failed for {uri}') from e
887
+ except Exception as e:
888
+ self.print_stderr(
889
+ f'ERROR: Exception ({e.__class__.__name__}) POSTing data ({request_id}) to {uri}: {e}'
890
+ )
891
+ raise Exception(f'ERROR: The SCANOSS Decoration API request failed for {uri}') from e
892
+ return None
893
+
894
+ def _call_rest(self, endpoint_key: str, request_input: dict, debug_msg: Optional[str] = None) -> Optional[Dict]:
895
+ """
896
+ Call a REST endpoint and return the response as a dictionary
897
+
898
+ Args:
899
+ endpoint_key (str): The key to lookup the REST endpoint in REST_ENDPOINTS
900
+ request_input (dict): The request data to send
901
+ debug_msg (str, optional): Debug message template that can include {rqId} placeholder.
902
+
903
+ Returns:
904
+ dict: The parsed REST response as a dictionary, or None if something went wrong
905
+ """
906
+ if endpoint_key not in REST_ENDPOINTS:
907
+ raise ScanossGrpcError(f'Unknown REST endpoint key: {endpoint_key}')
908
+
909
+ endpoint_config = REST_ENDPOINTS[endpoint_key]
910
+ endpoint_path = endpoint_config['path']
911
+ method = endpoint_config['method']
912
+ endpoint_url = f'{self.orig_url}{DEFAULT_URI_PREFIX}{endpoint_path}'
913
+ request_id = str(uuid.uuid4())
914
+
915
+ if debug_msg:
916
+ self.print_debug(debug_msg.format(rqId=request_id))
917
+
918
+ if method == 'GET':
919
+ response = self._rest_get(endpoint_url, request_id, params=request_input)
920
+ else: # POST
921
+ response = self._rest_post(endpoint_url, request_id, request_input)
922
+
923
+ if response and 'status' in response and not self.check_status_response_rest(response['status'], request_id):
924
+ return None
925
+
926
+ return response
927
+
416
928
 
417
929
  #
418
930
  # End of ScanossGrpc Class
419
931
  #
932
+
933
+
934
+ @dataclass
935
+ class GrpcConfig:
936
+ url: str = DEFAULT_URL
937
+ api_key: Optional[str] = SCANOSS_API_KEY
938
+ debug: Optional[bool] = False
939
+ trace: Optional[bool] = False
940
+ quiet: Optional[bool] = False
941
+ ver_details: Optional[str] = None
942
+ ca_cert: Optional[str] = None
943
+ timeout: Optional[int] = DEFAULT_TIMEOUT
944
+ proxy: Optional[str] = None
945
+ grpc_proxy: Optional[str] = None
946
+ pac: Optional[PACFile] = None
947
+ req_headers: Optional[dict] = None
948
+
949
+
950
+ def create_grpc_config_from_args(args) -> GrpcConfig:
951
+ return GrpcConfig(
952
+ url=getattr(args, 'api2url', DEFAULT_URL),
953
+ api_key=getattr(args, 'key', SCANOSS_API_KEY),
954
+ debug=getattr(args, 'debug', False),
955
+ trace=getattr(args, 'trace', False),
956
+ quiet=getattr(args, 'quiet', False),
957
+ ver_details=getattr(args, 'ver_details', None),
958
+ ca_cert=getattr(args, 'ca_cert', None),
959
+ timeout=getattr(args, 'timeout', DEFAULT_TIMEOUT),
960
+ proxy=getattr(args, 'proxy', None),
961
+ grpc_proxy=getattr(args, 'grpc_proxy', None),
962
+ )
963
+
964
+
965
+ #
966
+ # End of GrpcConfig class
967
+ #