Skip to content

Commit 84eee42

Browse files
authored
Add REST support for dependencies and vulnerabilities
* add REST support for dependencies and vulnerabilities * updated scanoss-py to v1.32.0 * fix issue with api key as evn
1 parent 693425b commit 84eee42

File tree

7 files changed

+261
-99
lines changed

7 files changed

+261
-99
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- Upcoming changes...
1111

12+
## [1.32.0] - 2025-09-01
13+
### Added
14+
- Switched vulnerability and dependency APIs to use REST by default
15+
1216
## [1.31.5] - 2025-08-27
1317
### Added
1418
- Added jira markdown option for DT
@@ -655,4 +659,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
655659
[1.31.2]: https://github.com/scanoss/scanoss.py/compare/v1.31.1...v1.31.2
656660
[1.31.3]: https://github.com/scanoss/scanoss.py/compare/v1.31.2...v1.31.3
657661
[1.31.4]: https://github.com/scanoss/scanoss.py/compare/v1.31.3...v1.31.4
658-
[1.31.5]: https://github.com/scanoss/scanoss.py/compare/v1.31.4...v1.31.5
662+
[1.31.5]: https://github.com/scanoss/scanoss.py/compare/v1.31.4...v1.31.5
663+
[1.31.5]: https://github.com/scanoss/scanoss.py/compare/v1.31.5...v1.32.0

src/scanoss/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
THE SOFTWARE.
2323
"""
2424

25-
__version__ = '1.31.5'
25+
__version__ = '1.32.0'

src/scanoss/cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
308308
help='Retrieve vulnerabilities for the given components',
309309
)
310310
c_vulns.set_defaults(func=comp_vulns)
311+
c_vulns.add_argument('--grpc', action='store_true', help='Enable gRPC support')
311312

312313
# Component Sub-command: component semgrep
313314
c_semgrep = comp_sub.add_parser(
@@ -964,7 +965,7 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
964965
p.add_argument(
965966
'--apiurl', type=str, help='SCANOSS API URL (optional - default: https://api.osskb.org/scan/direct)'
966967
)
967-
p.add_argument('--ignore-cert-errors', action='store_true', help='Ignore certificate errors')
968+
p.add_argument('--grpc', action='store_true', help='Enable gRPC support')
968969

969970
# Global Scan/Fingerprint filter options
970971
for p in [p_scan, p_wfp]:
@@ -1055,6 +1056,7 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
10551056
type=str,
10561057
help='Headers to be sent on request (e.g., -hdr "Name: Value") - can be used multiple times',
10571058
)
1059+
p.add_argument('--ignore-cert-errors', action='store_true', help='Ignore certificate errors')
10581060

10591061
# Syft options
10601062
for p in [p_cs, p_dep]:
@@ -1418,6 +1420,7 @@ def scan(parser, args): # noqa: PLR0912, PLR0915
14181420
strip_snippet_ids=args.strip_snippet,
14191421
scan_settings=scan_settings,
14201422
req_headers=process_req_headers(args.header),
1423+
use_grpc=args.grpc
14211424
)
14221425
if args.wfp:
14231426
if not scanner.is_file_or_snippet_scan():
@@ -2144,6 +2147,8 @@ def comp_vulns(parser, args):
21442147
pac=pac_file,
21452148
timeout=args.timeout,
21462149
req_headers=process_req_headers(args.header),
2150+
ignore_cert_errors=args.ignore_cert_errors,
2151+
use_grpc=args.grpc,
21472152
)
21482153
if not comps.get_vulnerabilities(args.input, args.purl, args.output):
21492154
sys.exit(1)

src/scanoss/components.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def __init__( # noqa: PLR0913, PLR0915
5252
ca_cert: str = None,
5353
pac: PACFile = None,
5454
req_headers: dict = None,
55+
ignore_cert_errors: bool = False,
56+
use_grpc: bool = False,
5557
):
5658
"""
5759
Handle all component style requests
@@ -66,6 +68,9 @@ def __init__( # noqa: PLR0913, PLR0915
6668
:param grpc_proxy: Specific gRPC proxy (optional)
6769
:param ca_cert: TLS client certificate (optional)
6870
:param pac: Proxy Auto-Config file (optional)
71+
:param req_headers: Additional headers to send with requests (optional)
72+
:param ignore_cert_errors: Ignore TLS certificate errors (optional)
73+
:param use_grpc: Use gRPC instead of HTTP (optional)
6974
"""
7075
super().__init__(debug, trace, quiet)
7176
ver_details = Scanner.version_details()
@@ -82,14 +87,28 @@ def __init__( # noqa: PLR0913, PLR0915
8287
grpc_proxy=grpc_proxy,
8388
timeout=timeout,
8489
req_headers=req_headers,
90+
ignore_cert_errors=ignore_cert_errors,
91+
use_grpc=use_grpc,
8592
)
8693

87-
def load_purls(self, json_file: Optional[str] = None, purls: Optional[List[str]] = None) -> Optional[dict]:
94+
def load_comps(self, json_file: Optional[str] = None, purls: Optional[List[str]] = None)-> Optional[dict]:
95+
"""
96+
Load the specified components and return a dictionary
97+
98+
:param json_file: JSON Components file (optional)
99+
:param purls: list pf PURLs (optional)
100+
:return: Components Request dictionary or None
101+
"""
102+
return self.load_purls(json_file, purls, 'components')
103+
104+
def load_purls(self, json_file: Optional[str] = None, purls: Optional[List[str]] = None, field:str = 'purls'
105+
) -> Optional[dict]:
88106
"""
89107
Load the specified purls and return a dictionary
90108
91109
:param json_file: JSON PURL file (optional)
92110
:param purls: list of PURLs (optional)
111+
:param field: Name of the dictionary field to store the purls in (default: 'purls')
93112
:return: PURL Request dictionary or None
94113
"""
95114
if json_file:
@@ -109,14 +128,14 @@ def load_purls(self, json_file: Optional[str] = None, purls: Optional[List[str]]
109128
parsed_purls = []
110129
for p in purls:
111130
parsed_purls.append({'purl': p})
112-
purl_request = {'purls': parsed_purls}
131+
purl_request = {field: parsed_purls}
113132
else:
114133
self.print_stderr('ERROR: No purls specified to process.')
115134
return None
116-
purl_count = len(purl_request.get('purls', []))
117-
self.print_debug(f'Parsed Purls ({purl_count}): {purl_request}')
135+
purl_count = len(purl_request.get(field, []))
136+
self.print_debug(f'Parsed {field} ({purl_count}): {purl_request}')
118137
if purl_count == 0:
119-
self.print_stderr('ERROR: No PURLs parsed from request.')
138+
self.print_stderr(f'ERROR: No {field} parsed from request.')
120139
return None
121140
return purl_request
122141

@@ -142,8 +161,8 @@ def _open_file_or_sdtout(self, filename):
142161
"""
143162
Open the given filename if requested, otherwise return STDOUT
144163
145-
:param filename:
146-
:return:
164+
:param filename: filename to open or None to return STDOUT
165+
:return: file descriptor or None
147166
"""
148167
file = sys.stdout
149168
if filename:
@@ -202,7 +221,7 @@ def get_vulnerabilities(self, json_file: str = None, purls: [] = None, output_fi
202221
:return: True on success, False otherwise
203222
"""
204223
success = False
205-
purls_request = self.load_purls(json_file, purls)
224+
purls_request = self.load_comps(json_file, purls)
206225
if purls_request is None or len(purls_request) == 0:
207226
return False
208227
file = self._open_file_or_sdtout(output_file)

src/scanoss/scanner.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def __init__( # noqa: PLR0913, PLR0915
107107
skip_md5_ids=None,
108108
scan_settings: 'ScanossSettings | None' = None,
109109
req_headers: dict = None,
110+
use_grpc: bool = False,
110111
):
111112
"""
112113
Initialise scanning class, including Winnowing, ScanossApi, ThreadedScanning
@@ -173,6 +174,8 @@ def __init__( # noqa: PLR0913, PLR0915
173174
pac=pac,
174175
grpc_proxy=grpc_proxy,
175176
req_headers=self.req_headers,
177+
ignore_cert_errors=ignore_cert_errors,
178+
use_grpc=use_grpc
176179
)
177180
self.threaded_deps = ThreadedDependencies(sc_deps, grpc_api, debug=debug, quiet=quiet, trace=trace)
178181
self.nb_threads = nb_threads

src/scanoss/scanossapi.py

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,23 @@
2222
THE SOFTWARE.
2323
"""
2424

25+
import http.client as http_client
2526
import logging
2627
import os
2728
import sys
2829
import time
30+
import uuid
2931
from json.decoder import JSONDecodeError
32+
3033
import requests
31-
import uuid
32-
import http.client as http_client
3334
import urllib3
34-
3535
from pypac import PACSession
3636
from pypac.parser import PACFile
3737
from urllib3.exceptions import InsecureRequestWarning
3838

39-
from .scanossbase import ScanossBase
4039
from . import __version__
41-
40+
from .constants import DEFAULT_TIMEOUT, MIN_TIMEOUT
41+
from .scanossbase import ScanossBase
4242

4343
DEFAULT_URL = 'https://api.osskb.org/scan/direct' # default free service URL
4444
DEFAULT_URL2 = 'https://api.scanoss.com/scan/direct' # default premium service URL
@@ -52,7 +52,7 @@ class ScanossApi(ScanossBase):
5252
Currently support posting scan requests to the SCANOSS streaming API
5353
"""
5454

55-
def __init__( # noqa: PLR0913, PLR0915
55+
def __init__( # noqa: PLR0912, PLR0913, PLR0915
5656
self,
5757
scan_format: str = None,
5858
flags: str = None,
@@ -61,7 +61,7 @@ def __init__( # noqa: PLR0913, PLR0915
6161
debug: bool = False,
6262
trace: bool = False,
6363
quiet: bool = False,
64-
timeout: int = 180,
64+
timeout: int = DEFAULT_TIMEOUT,
6565
ver_details: str = None,
6666
ignore_cert_errors: bool = False,
6767
proxy: str = None,
@@ -87,30 +87,28 @@ def __init__( # noqa: PLR0913, PLR0915
8787
HTTPS_PROXY='http://<ip>:<port>'
8888
"""
8989
super().__init__(debug, trace, quiet)
90-
self.url = url
91-
self.api_key = api_key
9290
self.sbom = None
9391
self.scan_format = scan_format if scan_format else 'plain'
9492
self.flags = flags
95-
self.timeout = timeout if timeout > 5 else 180
93+
self.timeout = timeout if timeout > MIN_TIMEOUT else DEFAULT_TIMEOUT
9694
self.retry_limit = retry if retry >= 0 else 5
9795
self.ignore_cert_errors = ignore_cert_errors
9896
self.req_headers = req_headers if req_headers else {}
9997
self.headers = {}
100-
98+
# Set the correct URL/API key combination
99+
self.url = url if url else SCANOSS_SCAN_URL
100+
self.api_key = api_key if api_key else SCANOSS_API_KEY
101+
if self.api_key and not url and not os.environ.get('SCANOSS_SCAN_URL'):
102+
self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium
101103
if ver_details:
102104
self.headers['x-scanoss-client'] = ver_details
103105
if self.api_key:
104106
self.headers['X-Session'] = self.api_key
105107
self.headers['x-api-key'] = self.api_key
106-
self.headers['User-Agent'] = f'scanoss-py/{__version__}'
107-
self.headers['user-agent'] = f'scanoss-py/{__version__}'
108-
self.load_generic_headers()
109-
110-
self.url = url if url else SCANOSS_SCAN_URL
111-
self.api_key = api_key if api_key else SCANOSS_API_KEY
112-
if self.api_key and not url and not os.environ.get('SCANOSS_SCAN_URL'):
113-
self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium
108+
user_agent = f'scanoss-py/{__version__}'
109+
self.headers['User-Agent'] = user_agent
110+
self.headers['user-agent'] = user_agent
111+
self.load_generic_headers(url)
114112

115113
if self.trace:
116114
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
@@ -133,7 +131,7 @@ def __init__( # noqa: PLR0913, PLR0915
133131
if self.proxies:
134132
self.session.proxies = self.proxies
135133

136-
def scan(self, wfp: str, context: str = None, scan_id: int = None):
134+
def scan(self, wfp: str, context: str = None, scan_id: int = None): # noqa: PLR0912, PLR0915
137135
"""
138136
Scan the specified WFP and return the JSON object
139137
:param wfp: WFP to scan
@@ -192,7 +190,7 @@ def scan(self, wfp: str, context: str = None, scan_id: int = None):
192190
else:
193191
self.print_stderr(f'Warning: No response received from {self.url}. Retrying...')
194192
time.sleep(5)
195-
elif r.status_code == 503: # Service limits have most likely been reached
193+
elif r.status_code == requests.codes.service_unavailable: # Service limits most likely reached
196194
self.print_stderr(
197195
f'ERROR: SCANOSS API rejected the scan request ({request_id}) due to '
198196
f'service limits being exceeded'
@@ -202,7 +200,7 @@ def scan(self, wfp: str, context: str = None, scan_id: int = None):
202200
f'ERROR: {r.status_code} - The SCANOSS API request ({request_id}) rejected '
203201
f'for {self.url} due to service limits being exceeded.'
204202
)
205-
elif r.status_code >= 400:
203+
elif r.status_code >= requests.codes.bad_request:
206204
if retry > self.retry_limit: # No response retry_limit or more times, fail
207205
self.save_bad_req_wfp(scan_files, request_id, scan_id)
208206
raise Exception(
@@ -269,7 +267,7 @@ def set_sbom(self, sbom):
269267
self.sbom = sbom
270268
return self
271269

272-
def load_generic_headers(self):
270+
def load_generic_headers(self, url):
273271
"""
274272
Adds custom headers from req_headers to the headers collection.
275273
@@ -279,7 +277,7 @@ def load_generic_headers(self):
279277
if self.req_headers: # Load generic headers
280278
for key, value in self.req_headers.items():
281279
if key == 'x-api-key': # Set premium URL if x-api-key header is set
282-
if not self.url and not os.environ.get('SCANOSS_SCAN_URL'):
280+
if not url and not os.environ.get('SCANOSS_SCAN_URL'):
283281
self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium
284282
self.api_key = value
285283
self.headers[key] = value

0 commit comments

Comments
 (0)