scanoss 1.43.1__py3-none-any.whl → 1.45.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.
scanoss/__init__.py CHANGED
@@ -22,4 +22,4 @@ SPDX-License-Identifier: MIT
22
22
  THE SOFTWARE.
23
23
  """
24
24
 
25
- __version__ = '1.43.1'
25
+ __version__ = '1.45.0'
scanoss/cli.py CHANGED
@@ -190,6 +190,44 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
190
190
  '--no-wfp-output', action='store_true',
191
191
  help='DEPRECATED: Scans no longer generate scanner_output.wfp. Use "fingerprint -o" to create WFP files.'
192
192
  )
193
+ p_scan.add_argument(
194
+ '--wfp-output', type=str, metavar='FILE',
195
+ help='Save fingerprints to specified file during scan'
196
+ )
197
+
198
+ # Snippet tuning options
199
+ p_scan.add_argument(
200
+ '--min-snippet-hits',
201
+ type=int,
202
+ default=None,
203
+ help='Minimum snippet hits required. A value of 0 defers to server configuration (optional)',
204
+ )
205
+ p_scan.add_argument(
206
+ '--min-snippet-lines',
207
+ type=int,
208
+ default=None,
209
+ help='Minimum snippet lines required. A value of 0 defers to server configuration (optional)',
210
+ )
211
+ p_scan.add_argument(
212
+ '--ranking',
213
+ type=str,
214
+ choices=['unset' ,'true', 'false'],
215
+ default='unset',
216
+ help='Enable or disable ranking (optional - default: server configuration)',
217
+ )
218
+ p_scan.add_argument(
219
+ '--ranking-threshold',
220
+ type=int,
221
+ default=-1,
222
+ help='Ranking threshold value. Valid range: -1 to 10. A value of -1 defers to server configuration (optional)',
223
+ )
224
+ p_scan.add_argument(
225
+ '--honour-file-exts',
226
+ type=str,
227
+ choices=['unset','true', 'false'],
228
+ default='unset',
229
+ help='Honour file extensions during scanning. When not set, defers to server configuration (optional)',
230
+ )
193
231
 
194
232
  # Sub-command: fingerprint
195
233
  p_wfp = subparsers.add_parser(
@@ -1076,7 +1114,7 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
1076
1114
  # Global Scan command options
1077
1115
  for p in [p_scan, p_cs]:
1078
1116
  p.add_argument(
1079
- '--apiurl', type=str, help='SCANOSS API URL (optional - default: https://api.osskb.org/scan/direct)'
1117
+ '--apiurl', type=str, help='SCANOSS API base URL (optional - default: https://api.osskb.org)'
1080
1118
  )
1081
1119
 
1082
1120
  # Global Scan/Fingerprint filter options
@@ -1377,11 +1415,11 @@ def wfp(parser, args):
1377
1415
  initialise_empty_file(args.output)
1378
1416
 
1379
1417
  # Load scan settings
1380
- scan_settings = None
1418
+ scanoss_settings = None
1381
1419
  if not args.skip_settings_file:
1382
- scan_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet)
1420
+ scanoss_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet)
1383
1421
  try:
1384
- scan_settings.load_json_file(args.settings, args.scan_dir)
1422
+ scanoss_settings.load_json_file(args.settings, args.scan_dir)
1385
1423
  except ScanossSettingsError as e:
1386
1424
  print_stderr(f'Error: {e}')
1387
1425
  sys.exit(1)
@@ -1403,7 +1441,7 @@ def wfp(parser, args):
1403
1441
  skip_md5_ids=args.skip_md5,
1404
1442
  strip_hpsm_ids=args.strip_hpsm,
1405
1443
  strip_snippet_ids=args.strip_snippet,
1406
- scan_settings=scan_settings,
1444
+ scanoss_settings=scanoss_settings,
1407
1445
  skip_headers=args.skip_headers,
1408
1446
  skip_headers_limit=args.skip_headers_limit,
1409
1447
  )
@@ -1487,20 +1525,20 @@ def scan(parser, args): # noqa: PLR0912, PLR0915
1487
1525
  print_stderr('ERROR: Cannot specify both --settings and --skip-file-settings options.')
1488
1526
  sys.exit(1)
1489
1527
  # Figure out which settings (if any) to load before processing
1490
- scan_settings = None
1528
+ scanoss_settings = None
1491
1529
  if not args.skip_settings_file:
1492
- scan_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet)
1530
+ scanoss_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet)
1493
1531
  try:
1494
1532
  if args.identify:
1495
- scan_settings.load_json_file(args.identify, args.scan_dir).set_file_type('legacy').set_scan_type(
1533
+ scanoss_settings.load_json_file(args.identify, args.scan_dir).set_file_type('legacy').set_scan_type(
1496
1534
  'identify'
1497
1535
  )
1498
1536
  elif args.ignore:
1499
- scan_settings.load_json_file(args.ignore, args.scan_dir).set_file_type('legacy').set_scan_type(
1537
+ scanoss_settings.load_json_file(args.ignore, args.scan_dir).set_file_type('legacy').set_scan_type(
1500
1538
  'blacklist'
1501
1539
  )
1502
1540
  else:
1503
- scan_settings.load_json_file(args.settings, args.scan_dir).set_file_type('new')
1541
+ scanoss_settings.load_json_file(args.settings, args.scan_dir).set_file_type('new')
1504
1542
 
1505
1543
  except ScanossSettingsError as e:
1506
1544
  print_stderr(f'Error: {e}')
@@ -1596,11 +1634,17 @@ def scan(parser, args): # noqa: PLR0912, PLR0915
1596
1634
  skip_md5_ids=args.skip_md5,
1597
1635
  strip_hpsm_ids=args.strip_hpsm,
1598
1636
  strip_snippet_ids=args.strip_snippet,
1599
- scan_settings=scan_settings,
1637
+ scanoss_settings=scanoss_settings,
1600
1638
  req_headers=process_req_headers(args.header),
1601
1639
  use_grpc=args.grpc,
1640
+ min_snippet_hits=args.min_snippet_hits,
1641
+ min_snippet_lines=args.min_snippet_lines,
1642
+ ranking=args.ranking,
1643
+ ranking_threshold=args.ranking_threshold,
1644
+ honour_file_exts=args.honour_file_exts,
1602
1645
  skip_headers=args.skip_headers,
1603
1646
  skip_headers_limit=args.skip_headers_limit,
1647
+ wfp_output=args.wfp_output,
1604
1648
  )
1605
1649
  if args.wfp:
1606
1650
  if not scanner.is_file_or_snippet_scan():
@@ -1 +1 @@
1
- date: 20260105120224, utime: 1767614544
1
+ date: 20260202142827, utime: 1770042507
@@ -139,6 +139,100 @@
139
139
  }
140
140
  }
141
141
  }
142
+ },
143
+ "file_snippet": {
144
+ "type": "object",
145
+ "description": "File snippet scanning configuration",
146
+ "properties": {
147
+ "proxy": {
148
+ "type": "object",
149
+ "description": "Proxy configuration for file snippet requests",
150
+ "properties": {
151
+ "host": {
152
+ "type": "string",
153
+ "description": "Proxy host URL"
154
+ }
155
+ }
156
+ },
157
+ "http_config": {
158
+ "type": "object",
159
+ "description": "HTTP configuration for file snippet requests",
160
+ "properties": {
161
+ "base_uri": {
162
+ "type": "string",
163
+ "description": "Base URI for file snippet API requests"
164
+ },
165
+ "ignore_cert_errors": {
166
+ "type": "boolean",
167
+ "description": "Whether to ignore certificate errors"
168
+ }
169
+ }
170
+ },
171
+ "ranking_enabled": {
172
+ "type": ["boolean", "null"],
173
+ "description": "Enable/disable ranking",
174
+ "default": null
175
+ },
176
+ "ranking_threshold": {
177
+ "type": ["integer", "null"],
178
+ "description": "Ranking threshold value. A value of -1 defers to server configuration",
179
+ "minimum": -1,
180
+ "maximum": 99,
181
+ "default": 0
182
+ },
183
+ "min_snippet_hits": {
184
+ "type": "integer",
185
+ "description": "Minimum snippet hits required",
186
+ "minimum": 0,
187
+ "default": 0
188
+ },
189
+ "min_snippet_lines": {
190
+ "type": "integer",
191
+ "description": "Minimum snippet lines required",
192
+ "minimum": 0,
193
+ "default": 0
194
+ },
195
+ "honour_file_exts": {
196
+ "type": ["boolean", "null"],
197
+ "description": "Ignores file extensions. When not set, defers to server configuration.",
198
+ "default": true
199
+ },
200
+ "dependency_analysis": {
201
+ "type": "boolean",
202
+ "description": "Enable dependency analysis"
203
+ },
204
+ "skip_headers": {
205
+ "type": "boolean",
206
+ "description": "Skip license headers, comments and imports at the beginning of files",
207
+ "default": false
208
+ },
209
+ "skip_headers_limit": {
210
+ "type": "integer",
211
+ "description": "Maximum number of lines to skip when filtering headers",
212
+ "default": 0
213
+ }
214
+ }
215
+ },
216
+ "hpfm": {
217
+ "type": "object",
218
+ "description": "HPFM (High Precision Folder Matching) configuration",
219
+ "properties": {
220
+ "ranking_enabled": {
221
+ "type": "boolean",
222
+ "description": "Enable ranking for HPFM"
223
+ },
224
+ "ranking_threshold": {
225
+ "type": ["integer", "null"],
226
+ "description": "Ranking threshold value. A value of -1 defers to server configuration",
227
+ "minimum": -1,
228
+ "maximum": 99,
229
+ "default": 0
230
+ }
231
+ }
232
+ },
233
+ "container": {
234
+ "type": "object",
235
+ "description": "Container scanning configuration"
142
236
  }
143
237
  }
144
238
  },
@@ -0,0 +1,311 @@
1
+ """
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2025, 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
+ """
24
+
25
+ from typing import TYPE_CHECKING, Optional
26
+
27
+ from .scanossbase import ScanossBase
28
+
29
+ if TYPE_CHECKING:
30
+ from .scanoss_settings import ScanossSettings
31
+
32
+ MAX_RANKING_THRESHOLD = 10
33
+
34
+
35
+ class ScanSettingsBuilder(ScanossBase):
36
+ """Builder class for merging CLI arguments with scanoss.json settings file values.
37
+
38
+ This class implements an API for merging scan configuration
39
+ from multiple sources with the following priority order:
40
+ 1. settings.file_snippet section in scanoss.json (highest priority)
41
+ 2. settings section in scanoss.json (middle priority)
42
+ 3. CLI arguments (lowest priority - used as fallback)
43
+
44
+ Attributes:
45
+ proxy: Merged proxy host URL
46
+ url: Merged API base URL
47
+ ignore_cert_errors: Whether to ignore SSL certificate errors
48
+ min_snippet_hits: Minimum snippet hits required for matching
49
+ min_snippet_lines: Minimum snippet lines required for matching
50
+ honour_file_exts: Whether to honour file extensions during scanning
51
+ ranking: Whether ranking is enabled
52
+ ranking_threshold: Ranking threshold value
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ scanoss_settings: 'ScanossSettings | None',
58
+ debug: bool = False,
59
+ trace: bool = False,
60
+ quiet: bool = False,
61
+ ):
62
+ """Initialize the builder with optional scanoss settings.
63
+
64
+ Args:
65
+ scanoss_settings: ScanossSettings instance loaded from scanoss.json,
66
+ or None if no settings file is available.
67
+ debug: Enable debug output
68
+ trace: Enable trace output
69
+ quiet: Enable quiet mode
70
+ """
71
+ super().__init__(debug=debug, trace=trace, quiet=quiet)
72
+ self.scanoss_settings = scanoss_settings
73
+ # Merged values
74
+ self.proxy: Optional[str] = None
75
+ self.url: Optional[str] = None
76
+ self.ignore_cert_errors: bool = False
77
+ self.min_snippet_hits: Optional[int] = None
78
+ self.min_snippet_lines: Optional[int] = None
79
+ self.honour_file_exts: Optional[any] = None
80
+ self.ranking: Optional[any] = None
81
+ self.ranking_threshold: Optional[int] = None
82
+
83
+ def with_proxy(self, cli_value: str = None) -> 'ScanSettingsBuilder':
84
+ """Set proxy host with priority: file_snippet.proxy.host > settings.proxy.host > CLI.
85
+
86
+ Args:
87
+ cli_value: Proxy host from CLI argument (e.g., 'http://proxy:8080')
88
+
89
+ Returns:
90
+ Self for method chaining
91
+ """
92
+ self.proxy = self._merge_with_priority(
93
+ cli_value,
94
+ self._get_proxy_host(self._get_file_snippet_proxy()),
95
+ self._get_proxy_host(self._get_root_proxy())
96
+ )
97
+ return self
98
+
99
+ def with_url(self, cli_value: str = None) -> 'ScanSettingsBuilder':
100
+ """Set API base URL with priority: file_snippet.http_config.base_uri > settings.http_config.base_uri > CLI.
101
+
102
+ Args:
103
+ cli_value: API base URL from CLI argument (e.g., 'https://api.scanoss.com')
104
+
105
+ Returns:
106
+ Self for method chaining
107
+ """
108
+ self.url = self._merge_with_priority(
109
+ cli_value,
110
+ self._get_file_snippet_http_config_value('base_uri'),
111
+ self._get_http_config_value('base_uri')
112
+ )
113
+ return self
114
+
115
+ def with_ignore_cert_errors(self, cli_value: bool = False) -> 'ScanSettingsBuilder':
116
+ """Set ignore_cert_errors with priority: CLI True > file_snippet > settings > False.
117
+
118
+ Note: CLI value only takes effect if True (flag present). False means
119
+ the flag was not provided, so settings file values are checked.
120
+
121
+ Args:
122
+ cli_value: Whether to ignore SSL certificate errors from CLI flag
123
+
124
+ Returns:
125
+ Self for method chaining
126
+ """
127
+ result = self._merge_with_priority(
128
+ cli_value if cli_value else None,
129
+ self._get_file_snippet_http_config_value('ignore_cert_errors'),
130
+ self._get_http_config_value('ignore_cert_errors')
131
+ )
132
+ self.ignore_cert_errors = result if result is not None else False
133
+ return self
134
+
135
+ def with_min_snippet_hits(self, cli_value: int = None) -> 'ScanSettingsBuilder':
136
+ """Set minimum snippet hits with priority: settings.file_snippet.min_snippet_hits > CLI.
137
+
138
+ Minimum allowed value is 0. Values below 0 will be clamped and logged.
139
+
140
+ Args:
141
+ cli_value: Minimum snippet hits from CLI argument
142
+
143
+ Returns:
144
+ Self for method chaining
145
+ """
146
+ self.min_snippet_hits = self._merge_cli_with_settings(
147
+ cli_value,
148
+ self._get_file_snippet_setting('min_snippet_hits')
149
+ )
150
+ if self.min_snippet_hits is not None and self.min_snippet_hits < 0:
151
+ self.print_msg(
152
+ f'WARNING: min-snippet-hits value {self.min_snippet_hits} is below minimum allowed (0). '
153
+ f'Setting to 0.'
154
+ )
155
+ self.min_snippet_hits = 0
156
+ return self
157
+
158
+ def with_min_snippet_lines(self, cli_value: int = None) -> 'ScanSettingsBuilder':
159
+ """Set minimum snippet lines with priority: settings.file_snippet.min_snippet_lines > CLI.
160
+
161
+ Minimum allowed value is 0. Values below 0 will be clamped and logged.
162
+
163
+ Args:
164
+ cli_value: Minimum snippet lines from CLI argument
165
+
166
+ Returns:
167
+ Self for method chaining
168
+ """
169
+ self.min_snippet_lines = self._merge_cli_with_settings(
170
+ cli_value,
171
+ self._get_file_snippet_setting('min_snippet_lines')
172
+ )
173
+ if self.min_snippet_lines is not None and self.min_snippet_lines < 0:
174
+ self.print_msg(
175
+ f'WARNING: min-snippet-lines value {self.min_snippet_lines} is below minimum allowed (0). '
176
+ f'Setting to 0.'
177
+ )
178
+ self.min_snippet_lines = 0
179
+ return self
180
+
181
+ def with_honour_file_exts(self, cli_value: str = None) -> 'ScanSettingsBuilder':
182
+ """Set honour_file_exts with priority: settings.file_snippet.honour_file_exts > CLI.
183
+
184
+ Args:
185
+ cli_value: String 'true', 'false', or 'unset' from CLI argument
186
+
187
+ Returns:
188
+ Self for method chaining
189
+ """
190
+ self.honour_file_exts = self._merge_cli_with_settings(
191
+ cli_value,
192
+ self._get_file_snippet_setting('honour_file_exts')
193
+ )
194
+ ## Convert to boolean
195
+ if self.honour_file_exts is not None and self.honour_file_exts!= 'unset':
196
+ self.honour_file_exts = self._str_to_bool(self.honour_file_exts)
197
+ return self
198
+
199
+ def with_ranking(self, cli_value: str = None) -> 'ScanSettingsBuilder':
200
+ """Set ranking enabled with priority: settings.file_snippet.ranking_enabled > CLI.
201
+
202
+ Args:
203
+ cli_value: String 'true', 'false', or 'unset' from CLI argument
204
+
205
+ Returns:
206
+ Self for method chaining
207
+ """
208
+ self.ranking = self._merge_cli_with_settings(
209
+ cli_value,
210
+ self._get_file_snippet_setting('ranking_enabled')
211
+ )
212
+ if self.ranking is not None and self.ranking != 'unset':
213
+ self.ranking = self._str_to_bool(self.ranking)
214
+ return self
215
+
216
+ def with_ranking_threshold(self, cli_value: int = None) -> 'ScanSettingsBuilder':
217
+ """Set ranking threshold with priority: settings.file_snippet.ranking_threshold > CLI.
218
+
219
+ Valid range is -1 to 10. Values outside this range will be clamped and logged.
220
+
221
+ Args:
222
+ cli_value: Ranking threshold from CLI argument
223
+
224
+ Returns:
225
+ Self for method chaining
226
+ """
227
+ self.ranking_threshold = self._merge_cli_with_settings(
228
+ cli_value,
229
+ self._get_file_snippet_setting('ranking_threshold')
230
+ )
231
+ if self.ranking_threshold is not None:
232
+ if self.ranking_threshold > MAX_RANKING_THRESHOLD:
233
+ self.print_msg(
234
+ f'WARNING: ranking-threshold value {self.ranking_threshold} exceeds maximum allowed '
235
+ f'({MAX_RANKING_THRESHOLD}). Setting to {MAX_RANKING_THRESHOLD}.'
236
+ )
237
+ self.ranking_threshold = MAX_RANKING_THRESHOLD
238
+ elif self.ranking_threshold < -1:
239
+ self.print_msg(
240
+ f'WARNING: ranking-threshold value {self.ranking_threshold} is below minimum allowed (-1). '
241
+ f'Setting to -1.'
242
+ )
243
+ self.ranking_threshold = -1
244
+ return self
245
+
246
+ # Private helper methods
247
+ @staticmethod
248
+ def _merge_with_priority(cli_value, file_snippet_value, root_value):
249
+ """Merge with priority: file_snippet > root settings > CLI"""
250
+ if file_snippet_value is not None:
251
+ return file_snippet_value
252
+ if root_value is not None:
253
+ return root_value
254
+ return cli_value
255
+
256
+ @staticmethod
257
+ def _merge_cli_with_settings(cli_value, settings_value):
258
+ """Merge CLI value with settings, with settings taking priority over CLI.
259
+
260
+ Returns settings_value if not None, otherwise falls back to cli_value.
261
+ """
262
+ if settings_value is not None:
263
+ return settings_value
264
+ return cli_value
265
+
266
+
267
+ @staticmethod
268
+ def _str_to_bool(value: str) -> Optional[bool]:
269
+ """Convert string 'true'/'false' to boolean."""
270
+ if value is None:
271
+ return None
272
+ if isinstance(value, bool):
273
+ return value
274
+ return value.lower() == 'true'
275
+
276
+ # Methods to extract values from scanoss_settings
277
+ def _get_file_snippet_setting(self, key: str):
278
+ """Get a setting from the file_snippet section."""
279
+ if not self.scanoss_settings:
280
+ return None
281
+ return self.scanoss_settings.get_file_snippet_settings().get(key)
282
+
283
+ def _get_file_snippet_proxy(self):
284
+ """Get proxy config from file_snippet section."""
285
+ return self.scanoss_settings.get_file_snippet_proxy() if self.scanoss_settings else None
286
+
287
+ def _get_root_proxy(self):
288
+ """Get proxy config from root settings section."""
289
+ return self.scanoss_settings.get_proxy() if self.scanoss_settings else None
290
+
291
+ @staticmethod
292
+ def _get_proxy_host(proxy_config) -> Optional[str]:
293
+ """Extract host from proxy configuration dict."""
294
+ if proxy_config is None:
295
+ return None
296
+ host = proxy_config.get('host')
297
+ return host if host else None
298
+
299
+ def _get_http_config_value(self, key: str):
300
+ """Extract a value from http_config dict."""
301
+ if not self.scanoss_settings:
302
+ return None
303
+ config = self.scanoss_settings.get_http_config()
304
+ return config.get(key) if config else None
305
+
306
+ def _get_file_snippet_http_config_value(self, key: str):
307
+ """Extract a value from file_snippet http_config dict."""
308
+ if not self.scanoss_settings:
309
+ return None
310
+ config = self.scanoss_settings.get_file_snippet_http_config()
311
+ return config.get(key) if config else None
scanoss/scanner.py CHANGED
@@ -39,6 +39,7 @@ from scanoss.file_filters import FileFilters
39
39
  from . import __version__
40
40
  from .csvoutput import CsvOutput
41
41
  from .cyclonedx import CycloneDx
42
+ from .scan_settings_builder import ScanSettingsBuilder
42
43
  from .scancodedeps import ScancodeDeps
43
44
  from .scanoss_settings import ScanossSettings
44
45
  from .scanossapi import ScanossApi
@@ -103,11 +104,17 @@ class Scanner(ScanossBase):
103
104
  strip_hpsm_ids=None,
104
105
  strip_snippet_ids=None,
105
106
  skip_md5_ids=None,
106
- scan_settings: 'ScanossSettings | None' = None,
107
+ scanoss_settings: 'ScanossSettings | None' = None,
107
108
  req_headers: dict = None,
108
109
  use_grpc: bool = False,
110
+ min_snippet_hits: int = None,
111
+ min_snippet_lines: int = None,
112
+ ranking: str = None,
113
+ ranking_threshold: int = None,
114
+ honour_file_exts: str = None,
109
115
  skip_headers: bool = False,
110
116
  skip_headers_limit: int = 0,
117
+ wfp_output: str = None,
111
118
  ):
112
119
  """
113
120
  Initialise scanning class, including Winnowing, ScanossApi, ThreadedScanning
@@ -119,6 +126,7 @@ class Scanner(ScanossBase):
119
126
  skip_extensions = []
120
127
  self.scan_output = scan_output
121
128
  self.output_format = output_format
129
+ self.wfp_output = wfp_output
122
130
  self.isatty = sys.stderr.isatty()
123
131
  self.all_extensions = all_extensions
124
132
  self.all_folders = all_folders
@@ -130,8 +138,20 @@ class Scanner(ScanossBase):
130
138
  self.skip_size = skip_size
131
139
  self.skip_extensions = skip_extensions
132
140
  self.req_headers = req_headers
141
+ self.scanoss_settings = scanoss_settings
133
142
  ver_details = Scanner.version_details()
134
143
 
144
+ # Get settings values for skip_headers options
145
+ file_snippet_settings = scanoss_settings.get_file_snippet_settings() if scanoss_settings else {}
146
+ settings_skip_headers = file_snippet_settings.get('skip_headers')
147
+ settings_skip_headers_limit = file_snippet_settings.get('skip_headers_limit')
148
+
149
+ # Merge CLI values with settings (scanoss.json takes priority over CLI)
150
+ skip_headers = Scanner._merge_cli_with_settings(skip_headers, settings_skip_headers)
151
+ skip_headers_limit = Scanner._merge_cli_with_settings(
152
+ skip_headers_limit, settings_skip_headers_limit)
153
+ self.print_debug(f'Skip headers {skip_headers} with limit: {skip_headers_limit}')
154
+
135
155
  self.winnowing = Winnowing(
136
156
  debug=debug,
137
157
  trace=trace,
@@ -146,21 +166,40 @@ class Scanner(ScanossBase):
146
166
  skip_headers=skip_headers,
147
167
  skip_headers_limit=skip_headers_limit,
148
168
  )
169
+
170
+ # Build merged settings using builder pattern
171
+ scan_settings = (ScanSettingsBuilder(scanoss_settings, debug=debug, trace=trace, quiet=quiet)
172
+ .with_proxy(proxy)
173
+ .with_url(url)
174
+ .with_ignore_cert_errors(ignore_cert_errors)
175
+ .with_min_snippet_hits(min_snippet_hits)
176
+ .with_min_snippet_lines(min_snippet_lines)
177
+ .with_honour_file_exts(honour_file_exts)
178
+ .with_ranking(ranking)
179
+ .with_ranking_threshold(ranking_threshold))
180
+
181
+ self.print_debug(f'Scan settings: {scan_settings}')
182
+
149
183
  self.scanoss_api = ScanossApi(
150
184
  debug=debug,
151
185
  trace=trace,
152
186
  quiet=quiet,
153
187
  api_key=api_key,
154
- url=url,
188
+ url=scan_settings.url,
155
189
  flags=flags,
156
190
  timeout=timeout,
157
191
  ver_details=ver_details,
158
- ignore_cert_errors=ignore_cert_errors,
159
- proxy=proxy,
192
+ ignore_cert_errors=scan_settings.ignore_cert_errors,
193
+ proxy=scan_settings.proxy,
160
194
  ca_cert=ca_cert,
161
195
  pac=pac,
162
196
  retry=retry,
163
- req_headers= self.req_headers,
197
+ req_headers=self.req_headers,
198
+ min_snippet_hits=scan_settings.min_snippet_hits,
199
+ min_snippet_lines=scan_settings.min_snippet_lines,
200
+ honour_file_exts=scan_settings.honour_file_exts,
201
+ ranking=scan_settings.ranking,
202
+ ranking_threshold=scan_settings.ranking_threshold,
164
203
  )
165
204
  sc_deps = ScancodeDeps(debug=debug, quiet=quiet, trace=trace, timeout=sc_timeout, sc_command=sc_command)
166
205
  grpc_api = ScanossGrpc(
@@ -191,19 +230,32 @@ class Scanner(ScanossBase):
191
230
  if self._skip_snippets:
192
231
  self.max_post_size = 8 * 1024 # 8k Max post size if we're skipping snippets
193
232
 
194
- self.scan_settings = scan_settings
195
233
  self.post_processor = (
196
- ScanPostProcessor(scan_settings, debug=debug, trace=trace, quiet=quiet) if scan_settings else None
234
+ ScanPostProcessor(scanoss_settings, debug=debug, trace=trace, quiet=quiet) if scan_settings else None
197
235
  )
198
236
  self._maybe_set_api_sbom()
199
237
 
200
238
  def _maybe_set_api_sbom(self):
201
- if not self.scan_settings:
239
+ if not self.scanoss_settings:
202
240
  return
203
- sbom = self.scan_settings.get_sbom()
241
+ sbom = self.scanoss_settings.get_sbom()
204
242
  if sbom:
205
243
  self.scanoss_api.set_sbom(sbom)
206
244
 
245
+ @staticmethod
246
+ def _merge_cli_with_settings(cli_value, settings_value):
247
+ """Merge CLI value with settings value (two-level priority: settings > cli).
248
+
249
+ Args:
250
+ cli_value: Value from CLI argument
251
+ settings_value: Value from scanoss.json file_snippet settings
252
+ Returns:
253
+ Merged value with CLI taking priority over settings
254
+ """
255
+ if settings_value is not None:
256
+ return settings_value
257
+ return cli_value
258
+
207
259
  @staticmethod
208
260
  def __count_files_in_wfp_file(wfp_file: str):
209
261
  """
@@ -286,7 +338,8 @@ class Scanner(ScanossBase):
286
338
  """
287
339
  if self.scan_options & ScanType.SCAN_DEPENDENCIES.value:
288
340
  return True
289
- return False
341
+ file_snippet_settings = self.scanoss_settings.get_file_snippet_settings() if self.scanoss_settings else {}
342
+ return file_snippet_settings.get('dependency_analysis', False)
290
343
 
291
344
  def scan_folder_with_options( # noqa: PLR0913
292
345
  self,
@@ -354,7 +407,7 @@ class Scanner(ScanossBase):
354
407
  debug=self.debug,
355
408
  trace=self.trace,
356
409
  quiet=self.quiet,
357
- scanoss_settings=self.scan_settings,
410
+ scanoss_settings=self.scanoss_settings,
358
411
  all_extensions=self.all_extensions,
359
412
  all_folders=self.all_folders,
360
413
  hidden_files_folders=self.hidden_files_folders,
@@ -373,6 +426,7 @@ class Scanner(ScanossBase):
373
426
  file_count = 0 # count all files fingerprinted
374
427
  wfp_file_count = 0 # count number of files in each queue post
375
428
  scan_started = False
429
+ wfp_list = [] if self.wfp_output else None # Collect WFPs if output file is specified
376
430
 
377
431
  to_scan_files = file_filters.get_filtered_files_from_folder(scan_dir)
378
432
  for to_scan_file in to_scan_files:
@@ -387,6 +441,8 @@ class Scanner(ScanossBase):
387
441
  if wfp is None or wfp == '':
388
442
  self.print_debug(f'No WFP returned for {to_scan_file}. Skipping.')
389
443
  continue
444
+ if wfp_list is not None:
445
+ wfp_list.append(wfp)
390
446
  file_count += 1
391
447
  if self.threaded_scan:
392
448
  wfp_size = len(wfp.encode('utf-8'))
@@ -420,6 +476,10 @@ class Scanner(ScanossBase):
420
476
  self.threaded_scan.queue_add(scan_block) # Make sure all files have been submitted
421
477
 
422
478
  if file_count > 0:
479
+ if wfp_list is not None:
480
+ self.print_debug(f'Writing fingerprints to {self.wfp_output}')
481
+ with open(self.wfp_output, 'w') as f:
482
+ f.write(''.join(wfp_list))
423
483
  if self.threaded_scan:
424
484
  success = self.__run_scan_threaded(scan_started, file_count)
425
485
  else:
@@ -615,7 +675,7 @@ class Scanner(ScanossBase):
615
675
  debug=self.debug,
616
676
  trace=self.trace,
617
677
  quiet=self.quiet,
618
- scanoss_settings=self.scan_settings,
678
+ scanoss_settings=self.scanoss_settings,
619
679
  all_extensions=self.all_extensions,
620
680
  all_folders=self.all_folders,
621
681
  hidden_files_folders=self.hidden_files_folders,
@@ -633,6 +693,7 @@ class Scanner(ScanossBase):
633
693
  file_count = 0 # count all files fingerprinted
634
694
  wfp_file_count = 0 # count number of files in each queue post
635
695
  scan_started = False
696
+ wfp_list = [] if self.wfp_output else None # Collect WFPs if output file is specified
636
697
 
637
698
  to_scan_files = file_filters.get_filtered_files_from_files(files)
638
699
  for file in to_scan_files:
@@ -646,6 +707,8 @@ class Scanner(ScanossBase):
646
707
  if wfp is None or wfp == '':
647
708
  self.print_debug(f'No WFP returned for {file}. Skipping.')
648
709
  continue
710
+ if wfp_list is not None:
711
+ wfp_list.append(wfp)
649
712
  file_count += 1
650
713
  if self.threaded_scan:
651
714
  wfp_size = len(wfp.encode('utf-8'))
@@ -680,6 +743,10 @@ class Scanner(ScanossBase):
680
743
  self.threaded_scan.queue_add(scan_block) # Make sure all files have been submitted
681
744
 
682
745
  if file_count > 0:
746
+ if wfp_list is not None:
747
+ self.print_debug(f'Writing fingerprints to {self.wfp_output}')
748
+ with open(self.wfp_output, 'w') as f:
749
+ f.write(''.join(wfp_list))
683
750
  if self.threaded_scan:
684
751
  success = self.__run_scan_threaded(scan_started, file_count)
685
752
  else:
@@ -923,7 +990,7 @@ class Scanner(ScanossBase):
923
990
  debug=self.debug,
924
991
  trace=self.trace,
925
992
  quiet=self.quiet,
926
- scanoss_settings=self.scan_settings,
993
+ scanoss_settings=self.scanoss_settings,
927
994
  all_extensions=self.all_extensions,
928
995
  all_folders=self.all_folders,
929
996
  hidden_files_folders=self.hidden_files_folders,
@@ -335,3 +335,99 @@ class ScanossSettings(ScanossBase):
335
335
  List: Min and max sizes to skip
336
336
  """
337
337
  return self.data.get('settings', {}).get('skip', {}).get('sizes', {}).get(operation_type, [])
338
+
339
+ def get_file_snippet_settings(self) -> dict:
340
+ """
341
+ Get the file_snippet settings section
342
+ Returns:
343
+ dict: File snippet settings
344
+ """
345
+ return self.data.get('settings', {}).get('file_snippet', {})
346
+
347
+ def get_min_snippet_hits(self) -> Optional[int]:
348
+ """
349
+ Get the minimum snippet hits required
350
+ Returns:
351
+ int or None: Minimum snippet hits, or None if not set
352
+ """
353
+ return self.get_file_snippet_settings().get('min_snippet_hits')
354
+
355
+ def get_min_snippet_lines(self) -> Optional[int]:
356
+ """
357
+ Get the minimum snippet lines required
358
+ Returns:
359
+ int or None: Minimum snippet lines, or None if not set
360
+ """
361
+ return self.get_file_snippet_settings().get('min_snippet_lines')
362
+
363
+ def get_ranking_enabled(self) -> Optional[bool]:
364
+ """
365
+ Get whether ranking is enabled
366
+ Returns:
367
+ bool or None: True if enabled, False if disabled, None if not set
368
+ """
369
+ return self.get_file_snippet_settings().get('ranking_enabled')
370
+
371
+ def get_ranking_threshold(self) -> Optional[int]:
372
+ """
373
+ Get the ranking threshold value
374
+ Returns:
375
+ int or None: Ranking threshold, or None if not set
376
+ """
377
+ return self.get_file_snippet_settings().get('ranking_threshold')
378
+
379
+ def get_honour_file_exts(self) -> Optional[bool]:
380
+ """
381
+ Get whether to honour file extensions
382
+ Returns:
383
+ bool or None: True to honour, False to ignore, None if not set
384
+ """
385
+ return self.get_file_snippet_settings().get('honour_file_exts')
386
+
387
+ def get_skip_headers_limit(self) -> int:
388
+ """
389
+ Get the skip headers limit value
390
+ Returns:
391
+ int: Skip headers limit, or 0 if not set
392
+ """
393
+ return self.get_file_snippet_settings().get('skip_headers_limit', 0)
394
+
395
+ def get_skip_headers(self) -> bool:
396
+ """
397
+ Get whether to skip headers
398
+ Returns:
399
+ bool: True to skip headers, False otherwise (default)
400
+ """
401
+ return self.get_file_snippet_settings().get('skip_headers', False)
402
+
403
+ def get_proxy(self) -> Optional[dict]:
404
+ """
405
+ Get the root-level proxy configuration
406
+ Returns:
407
+ dict or None: Proxy configuration with 'host' key, or None if not set
408
+ """
409
+ return self.data.get('settings', {}).get('proxy')
410
+
411
+ def get_http_config(self) -> Optional[dict]:
412
+ """
413
+ Get the root-level http_config configuration
414
+ Returns:
415
+ dict or None: HTTP config with 'base_uri' and 'ignore_cert_errors' keys, or None if not set
416
+ """
417
+ return self.data.get('settings', {}).get('http_config')
418
+
419
+ def get_file_snippet_proxy(self) -> Optional[dict]:
420
+ """
421
+ Get the file_snippet-level proxy configuration (takes priority over root)
422
+ Returns:
423
+ dict or None: Proxy configuration with 'host' key, or None if not set
424
+ """
425
+ return self.get_file_snippet_settings().get('proxy')
426
+
427
+ def get_file_snippet_http_config(self) -> Optional[dict]:
428
+ """
429
+ Get the file_snippet-level http_config configuration (takes priority over root)
430
+ Returns:
431
+ dict or None: HTTP config with 'base_uri' and 'ignore_cert_errors' keys, or None if not set
432
+ """
433
+ return self.get_file_snippet_settings().get('http_config')
scanoss/scanossapi.py CHANGED
@@ -22,13 +22,17 @@ SPDX-License-Identifier: MIT
22
22
  THE SOFTWARE.
23
23
  """
24
24
 
25
+ import base64
25
26
  import http.client as http_client
27
+ import json
26
28
  import logging
27
29
  import os
28
30
  import sys
29
31
  import time
30
32
  import uuid
31
33
  from json.decoder import JSONDecodeError
34
+ from typing import Optional, Union
35
+ from urllib.parse import urlparse, urlunparse
32
36
 
33
37
  import requests
34
38
  import urllib3
@@ -40,8 +44,9 @@ from . import __version__
40
44
  from .constants import DEFAULT_TIMEOUT, MIN_TIMEOUT
41
45
  from .scanossbase import ScanossBase
42
46
 
43
- DEFAULT_URL = 'https://api.osskb.org/scan/direct' # default free service URL
44
- DEFAULT_URL2 = 'https://api.scanoss.com/scan/direct' # default premium service URL
47
+ DEFAULT_URL = 'https://api.osskb.org' # default free service base URL
48
+ DEFAULT_URL2 = 'https://api.scanoss.com' # default premium service base URL
49
+ SCAN_ENDPOINT = '/scan/direct' # scan endpoint path
45
50
  SCANOSS_SCAN_URL = os.environ.get('SCANOSS_SCAN_URL') if os.environ.get('SCANOSS_SCAN_URL') else DEFAULT_URL
46
51
  SCANOSS_API_KEY = os.environ.get('SCANOSS_API_KEY') if os.environ.get('SCANOSS_API_KEY') else ''
47
52
 
@@ -52,6 +57,33 @@ class ScanossApi(ScanossBase):
52
57
  Currently support posting scan requests to the SCANOSS streaming API
53
58
  """
54
59
 
60
+ def normalize_api_url(self, url: str) -> str:
61
+ """
62
+ Normalize API URL to ensure it's a base URL with the scan endpoint appended.
63
+
64
+ If the URL contains a path component (e.g., /scan/direct), a warning is emitted
65
+ and the path is stripped to use only the base URL.
66
+
67
+ :param url: Input URL (can be base URL or full endpoint URL)
68
+ :return: Normalized URL with /scan/direct endpoint
69
+ """
70
+ if not url:
71
+ return url
72
+
73
+ url = url.strip()
74
+ parsed = urlparse(url)
75
+
76
+ if parsed.path and parsed.path != '/':
77
+ self.print_stderr(
78
+ f"Warning: URL '{url}' contains path '{parsed.path}'. "
79
+ f"Using base URL only: '{parsed.scheme}://{parsed.netloc}'"
80
+ )
81
+ base_url = urlunparse((parsed.scheme, parsed.netloc, '', '', '', ''))
82
+ else:
83
+ base_url = url.rstrip('/')
84
+
85
+ return f'{base_url}{SCAN_ENDPOINT}'
86
+
55
87
  def __init__( # noqa: PLR0912, PLR0913, PLR0915
56
88
  self,
57
89
  scan_format: str = None,
@@ -69,16 +101,26 @@ class ScanossApi(ScanossBase):
69
101
  pac: PACFile = None,
70
102
  retry: int = 5,
71
103
  req_headers: dict = None,
104
+ min_snippet_hits: int = None,
105
+ min_snippet_lines: int = None,
106
+ honour_file_exts: Union[bool, str, None] = 'unset',
107
+ ranking: Union[bool, str, None] = 'unset',
108
+ ranking_threshold: int = None,
72
109
  ):
73
110
  """
74
111
  Initialise the SCANOSS API
75
112
  :param scan_format: Scan format (default plain)
76
113
  :param flags: Scanning flags (default None)
77
- :param url: API URL (default https://api.osskb.org/scan/direct)
114
+ :param url: API base URL (default https://api.osskb.org). The /scan/direct endpoint is automatically appended.
78
115
  :param api_key: API Key (default None)
79
116
  :param debug: Enable debug (default False)
80
117
  :param trace: Enable trace (default False)
81
118
  :param quiet: Enable quiet mode (default False)
119
+ :param min_snippet_hits: Minimum snippet hits required (default None)
120
+ :param min_snippet_lines: Minimum snippet lines required (default None)
121
+ :param honour_file_exts: Whether to honour file extensions (default 'unset')
122
+ :param ranking: Enable/disable ranking (default 'unset')
123
+ :param ranking_threshold: Ranking threshold value (default None)
82
124
 
83
125
  To set a custom certificate use:
84
126
  REQUESTS_CA_BUNDLE=/path/to/cert.pem
@@ -88,6 +130,12 @@ class ScanossApi(ScanossBase):
88
130
  """
89
131
  super().__init__(debug, trace, quiet)
90
132
  self.sbom = None
133
+ # Scan tuning parameters
134
+ self.min_snippet_hits = min_snippet_hits
135
+ self.min_snippet_lines = min_snippet_lines
136
+ self.honour_file_exts = honour_file_exts
137
+ self.ranking = ranking
138
+ self.ranking_threshold = ranking_threshold
91
139
  self.scan_format = scan_format if scan_format else 'plain'
92
140
  self.flags = flags
93
141
  self.timeout = timeout if timeout > MIN_TIMEOUT else DEFAULT_TIMEOUT
@@ -95,11 +143,11 @@ class ScanossApi(ScanossBase):
95
143
  self.ignore_cert_errors = ignore_cert_errors
96
144
  self.req_headers = req_headers if req_headers else {}
97
145
  self.headers = {}
98
- # Set the correct URL/API key combination
99
- self.url = url if url else SCANOSS_SCAN_URL
146
+ base_url = url if url else SCANOSS_SCAN_URL
100
147
  self.api_key = api_key if api_key else SCANOSS_API_KEY
101
148
  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
149
+ base_url = DEFAULT_URL2
150
+ self.url = self.normalize_api_url(base_url)
103
151
  if ver_details:
104
152
  self.headers['x-scanoss-client'] = ver_details
105
153
  if self.api_key:
@@ -113,7 +161,7 @@ class ScanossApi(ScanossBase):
113
161
  if self.trace:
114
162
  logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
115
163
  http_client.HTTPConnection.debuglevel = 1
116
- if pac and not proxy: # Setup PAC session if requested (and no proxy has been explicitly set)
164
+ if pac and not proxy:
117
165
  self.print_debug('Setting up PAC session...')
118
166
  self.session = PACSession(pac=pac)
119
167
  else:
@@ -154,6 +202,10 @@ class ScanossApi(ScanossBase):
154
202
  scan_files = {'file': ('%s.wfp' % request_id, wfp)}
155
203
  headers = self.headers
156
204
  headers['x-request-id'] = request_id # send a unique request id for each post
205
+ # Add scan settings header if any settings are configured
206
+ scan_settings_header = self._build_scan_settings_header()
207
+ if scan_settings_header:
208
+ headers['scanoss-settings'] = scan_settings_header
157
209
  r = None
158
210
  retry = 0 # Add some retry logic to cater for timeouts, etc.
159
211
  while retry <= self.retry_limit:
@@ -267,21 +319,58 @@ class ScanossApi(ScanossBase):
267
319
  self.sbom = sbom
268
320
  return self
269
321
 
322
+ def _build_scan_settings_header(self) -> Optional[str]:
323
+ """
324
+ Build base64-encoded JSON for x-scanoss-scan-settings header.
325
+ Only includes parameters that have meaningful (non-"unset") values.
326
+ Returns:
327
+ Base64-encoded JSON string, or None if no settings to send
328
+ """
329
+ settings = {}
330
+
331
+ # min_snippet_hits: 0 = unset, don't send
332
+ if self.min_snippet_hits is not None and self.min_snippet_hits != 0:
333
+ settings['min_snippet_hits'] = self.min_snippet_hits
334
+
335
+ # min_snippet_lines: 0 = unset, don't send
336
+ if self.min_snippet_lines is not None and self.min_snippet_lines != 0:
337
+ settings['min_snippet_lines'] = self.min_snippet_lines
338
+
339
+ # honour_file_exts: None = unset, don't send
340
+ if self.honour_file_exts is not None and self.honour_file_exts != 'unset':
341
+ settings['honour_file_exts'] = self.honour_file_exts
342
+
343
+ # ranking: None = unset, don't send
344
+ if self.ranking is not None and self.ranking != 'unset':
345
+ settings['ranking_enabled'] = self.ranking
346
+
347
+ # ranking_threshold: -1 = unset, don't send
348
+ if self.ranking_threshold is not None and self.ranking_threshold != -1:
349
+ settings['ranking_threshold'] = self.ranking_threshold
350
+
351
+ if settings:
352
+ json_str = json.dumps(settings)
353
+ self.print_debug(f'Scan settings: {json_str}')
354
+ return base64.b64encode(json_str.encode()).decode()
355
+ return None
356
+
270
357
  def load_generic_headers(self, url):
271
358
  """
272
- Adds custom headers from req_headers to the headers collection.
359
+ Adds custom headers from req_headers to the headers collection.
273
360
 
274
- If x-api-key is present and no URL is configured (directly or via
275
- environment), sets URL to the premium endpoint (DEFAULT_URL2).
276
- """
361
+ If x-api-key is present and no URL is configured (directly or via
362
+ environment), sets URL to the premium endpoint (DEFAULT_URL2).
363
+ """
277
364
  if self.req_headers: # Load generic headers
278
365
  for key, value in self.req_headers.items():
279
- if key == 'x-api-key': # Set premium URL if x-api-key header is set
366
+ if key == 'x-api-key': # Set premium URL if x-api-key header is set
280
367
  if not url and not os.environ.get('SCANOSS_SCAN_URL'):
281
- self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium
368
+ # API key specific and no alternative URL, so use the default premium
369
+ self.url = self.normalize_api_url(DEFAULT_URL2)
282
370
  self.api_key = value
283
371
  self.headers[key] = value
284
372
 
373
+
285
374
  #
286
375
  # End of ScanossApi Class
287
376
  #
@@ -80,7 +80,7 @@ class ScanPostProcessor(ScanossBase):
80
80
 
81
81
  def __init__(
82
82
  self,
83
- scan_settings: ScanossSettings,
83
+ scanoss_settings: ScanossSettings,
84
84
  debug: bool = False,
85
85
  trace: bool = False,
86
86
  quiet: bool = False,
@@ -88,14 +88,14 @@ class ScanPostProcessor(ScanossBase):
88
88
  ):
89
89
  """
90
90
  Args:
91
- scan_settings (ScanossSettings): Scan settings object
91
+ scanoss_settings (ScanossSettings): Scanoss settings object
92
92
  debug (bool, optional): Debug mode. Defaults to False.
93
93
  trace (bool, optional): Traces. Defaults to False.
94
94
  quiet (bool, optional): Quiet mode. Defaults to False.
95
95
  results (dict | str, optional): Results to be processed. Defaults to None.
96
96
  """
97
97
  super().__init__(debug, trace, quiet)
98
- self.scan_settings = scan_settings
98
+ self.scanoss_settings = scanoss_settings
99
99
  self.results: dict = results
100
100
  self.component_info_map: dict = {}
101
101
 
@@ -114,10 +114,10 @@ class ScanPostProcessor(ScanossBase):
114
114
  if not self.results:
115
115
  return
116
116
  for _, result in self.results.items():
117
- result = result[0] if isinstance(result, list) else result
118
- purls = result.get('purl', [])
117
+ entry = result[0] if isinstance(result, list) else result
118
+ purls = entry.get('purl', [])
119
119
  for purl in purls:
120
- self.component_info_map[purl] = result
120
+ self.component_info_map[purl] = entry
121
121
 
122
122
  def post_process(self):
123
123
  """
@@ -126,7 +126,7 @@ class ScanPostProcessor(ScanossBase):
126
126
  Returns:
127
127
  dict: Processed results
128
128
  """
129
- if self.scan_settings.is_legacy():
129
+ if self.scanoss_settings.is_legacy():
130
130
  self.print_stderr(
131
131
  'Legacy settings file detected. Post-processing is not supported for legacy settings file.'
132
132
  )
@@ -139,7 +139,7 @@ class ScanPostProcessor(ScanossBase):
139
139
  """
140
140
  Remove entries from the results based on files and/or purls specified in the SCANOSS settings file
141
141
  """
142
- to_remove_entries = self.scan_settings.get_bom_remove()
142
+ to_remove_entries = self.scanoss_settings.get_bom_remove()
143
143
  if not to_remove_entries:
144
144
  return
145
145
  self.results = {
@@ -152,15 +152,15 @@ class ScanPostProcessor(ScanossBase):
152
152
  """
153
153
  Replace purls in the results based on the SCANOSS settings file
154
154
  """
155
- to_replace_entries = self.scan_settings.get_bom_replace()
155
+ to_replace_entries = self.scanoss_settings.get_bom_replace()
156
156
  if not to_replace_entries:
157
157
  return
158
158
 
159
159
  for result_path, result in self.results.items():
160
- result = result[0] if isinstance(result, list) else result
161
- should_replace, to_replace_with_purl = self._should_replace_result(result_path, result, to_replace_entries)
160
+ entry = result[0] if isinstance(result, list) else result
161
+ should_replace, to_replace_with_purl = self._should_replace_result(result_path, entry, to_replace_entries)
162
162
  if should_replace:
163
- self.results[result_path] = [self._update_replaced_result(result, to_replace_with_purl)]
163
+ self.results[result_path] = [self._update_replaced_result(entry, to_replace_with_purl)]
164
164
 
165
165
  def _update_replaced_result(self, result: dict, to_replace_with_purl: str) -> dict:
166
166
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scanoss
3
- Version: 1.43.1
3
+ Version: 1.45.0
4
4
  Summary: Simple Python library to leverage the SCANOSS APIs
5
5
  Home-page: https://scanoss.com
6
6
  Author: SCANOSS
@@ -156,8 +156,8 @@ if __name__ == "__main__":
156
156
  ```
157
157
 
158
158
  ## Scanning URL and API Key
159
- By Default, scanoss uses the API URL endpoint for SCANOSS OSS KB: https://api.osskb.org/scan/direct.
160
- This API does not require an API key.
159
+ By Default, scanoss uses the API base URL for SCANOSS OSS KB: https://api.osskb.org.
160
+ The `/scan/direct` endpoint is automatically appended. This API does not require an API key.
161
161
 
162
162
  These values can be changed from the command line using:
163
163
  ```bash
@@ -6,8 +6,8 @@ protoc_gen_swagger/options/annotations_pb2_grpc.py,sha256=KZOW9Ciio-f9iL42FuLFnS
6
6
  protoc_gen_swagger/options/openapiv2_pb2.py,sha256=w0xDs63uyrWGgzRaQZXfJpfI7Jpyvh-i9ay_uzOR-aM,16475
7
7
  protoc_gen_swagger/options/openapiv2_pb2.pyi,sha256=hYOV6uQ2yqhP89042_V3GuAsvoBBiXf5CGuYmnFnfv4,54665
8
8
  protoc_gen_swagger/options/openapiv2_pb2_grpc.py,sha256=sje9Nh3yE7CHCUWZwtjTgwsKB4GvyGz5vOrGTnRXJfc,917
9
- scanoss/__init__.py,sha256=lwPI4-clDHq4wUHNYOI6cksqekMeupq-O8d0iRfUd_A,1146
10
- scanoss/cli.py,sha256=27gumZDiD5ui5ICmmaIaf8OM8LlAWZ2rlI4LC7DFvUU,104579
9
+ scanoss/__init__.py,sha256=iPkgzK0wSsFjSAwlOzqgAXcFJW65XbmsUg-xi_nfZUo,1146
10
+ scanoss/cli.py,sha256=oSw9-YTxZE90wUP4WECXVQh5Zzl_SGVA8NkcwdU3-4M,106155
11
11
  scanoss/components.py,sha256=NFyt_w3aoMotr_ZaFU-ng00_89sruc0kgY7ERnJXkmM,15891
12
12
  scanoss/constants.py,sha256=vurzLNIfP_dnRMwOdZsUWvr5XAVuGoj98XZ0yjXNOjQ,632
13
13
  scanoss/cryptography.py,sha256=lOoD_dW16ARQxYiYyb5R8S7gx0FqWIsnGkKfsB0nGaU,10627
@@ -20,13 +20,14 @@ scanoss/gitlabqualityreport.py,sha256=_VG0Xoh8wYF3lsXGJvjoj-Ty58OS_-H1Domiq9OpQE
20
20
  scanoss/header_filter.py,sha256=-Dqore9coROLMWWw9yP3nz8dpCB7jYAVm842hoRTmeE,21879
21
21
  scanoss/osadl.py,sha256=VWalcHpshWxtRDGje2cK32SfFeSBAO62knfSW9pyYqc,4558
22
22
  scanoss/results.py,sha256=47ZXXuU2sDjYa5vhtbWTmikit9jHhA0rsYKwkvZFI5w,9252
23
+ scanoss/scan_settings_builder.py,sha256=3AyQXJgBVZLWPyRhptACSB8LoMHdX3BSzRqeaYQIXR8,12143
23
24
  scanoss/scancodedeps.py,sha256=JbpoGW1POtPMmowzfwa4oh8sSBeeQCqaW9onvc4UFYM,11517
24
- scanoss/scanner.py,sha256=3dkkNwi4KSHeBMk0Pmjf3WJ6SA28-om2iGxuDsqnfGg,39778
25
- scanoss/scanoss_settings.py,sha256=W8uFQ6uRIqtE-DXXA56bO8I4GsbJ-aA1c84hQ_qBel4,12161
26
- scanoss/scanossapi.py,sha256=O1ZNH9Kt8JzhLVBxfOSmJdEwSJTDP-rA54DulYdE8e4,13243
25
+ scanoss/scanner.py,sha256=p2d4mbap5gaY5WVJPMsicZF0pPu9vbokUJcd9GGC77k,43193
26
+ scanoss/scanoss_settings.py,sha256=O0AD0XhXYiQaD2Ym3pmoj0kJrUuEvY9gfsSliDaQ40c,15600
27
+ scanoss/scanossapi.py,sha256=3wN5POEt-Hi3qz_8VFC0JqDw2QHliJ-dJIW8VNZEzc8,16906
27
28
  scanoss/scanossbase.py,sha256=tKlHPAi50ZarGaPXsNi1XrowQBynsSqSSst-NuG2ScI,3163
28
29
  scanoss/scanossgrpc.py,sha256=9UuVPUjBLUhqim_tSntyoRZW-OAtiz5iP_VjjNr5RPY,41715
29
- scanoss/scanpostprocessor.py,sha256=-JsThlxrU70r92GHykTMERnicdd-6jmwNsE4PH0MN2o,11063
30
+ scanoss/scanpostprocessor.py,sha256=u6ZfwkSukn8TjeqRj1_IuWNjlUEug1_B36vZNIfUPZk,11081
30
31
  scanoss/scantype.py,sha256=gFmyVmKQpHWogN2iCmMj032e_sZo4T92xS3_EH5B3Tc,1310
31
32
  scanoss/spdxlite.py,sha256=4JMxmyNmvcL6fjScihk8toWfSuQ-Pj1gzaT3SIn1fXA,29425
32
33
  scanoss/threadeddependencies.py,sha256=aN8E43iKS1pWJLJP3xCle5ewlfR5DE2-ljUzI_29Xwk,9851
@@ -67,9 +68,9 @@ scanoss/api/vulnerabilities/__init__.py,sha256=IFrDk_DTJgKSZmmU-nuLXuq_s8sQZlrSC
67
68
  scanoss/api/vulnerabilities/v2/__init__.py,sha256=IFrDk_DTJgKSZmmU-nuLXuq_s8sQZlrSCHhIDMJT4r0,1122
68
69
  scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py,sha256=pmm0MSiXkdf8e4rCIIDRcsNRixR2vGvD1Xak4l-wdwI,16550
69
70
  scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py,sha256=BNxT5kUKQ-mgtOt5QYBM1Qrg5LNDqSpWKpfEZquIlsM,19127
70
- scanoss/data/build_date.txt,sha256=iGs8QX_FYWZn7LBC-nwnjX8Ul1Sb9cklgACl5eIHb2Q,40
71
+ scanoss/data/build_date.txt,sha256=gyjPVgVGpUWx0EUA2CYrW4yKZV_gBf-3hDut1Va79ME,40
71
72
  scanoss/data/osadl-copyleft.json,sha256=O9b2XAfpjQY0TL0fYzO6kwMcp5IwQbF6f_YWbB10MhQ,4761
72
- scanoss/data/scanoss-settings-schema.json,sha256=ClkRYAkjAN0Sk704G8BE_Ok006oQ6YnIGmX84CF8h9w,8798
73
+ scanoss/data/scanoss-settings-schema.json,sha256=baOwoHm5_JumSOxWYp6COHqnmTR7eO9RxVwf3FAOuJY,12157
73
74
  scanoss/data/spdx-exceptions.json,sha256=s7UTYxC7jqQXr11YBlIWYCNwN6lRDFTR33Y8rpN_dA4,17953
74
75
  scanoss/data/spdx-licenses.json,sha256=A6Z0q82gaTLtnopBfzeIVZjJFxkdRW1g2TuumQc-lII,228794
75
76
  scanoss/export/__init__.py,sha256=D4C0lWLuNp8k_BjQZEc07WZcUgAvriVwQWOk063b0ZU,1122
@@ -102,9 +103,9 @@ scanoss/utils/crc64.py,sha256=TMrwQimSdE6imhFOUL7oAG6Kxu-8qMpGWMuMg8QpSVs,3169
102
103
  scanoss/utils/file.py,sha256=62cA9a17TU9ZvfA3FY5HY4-QOajJeSrc8S6xLA_f-3M,2980
103
104
  scanoss/utils/scanoss_scan_results_utils.py,sha256=ho9-DKefHFJlVZkw4gXOmMI-mgPIbV9Y2ftkI83fC1k,1727
104
105
  scanoss/utils/simhash.py,sha256=6iu8DOcecPAY36SZjCOzrrLMT9oIE7-gI6QuYwUQ7B0,5793
105
- scanoss-1.43.1.dist-info/licenses/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
106
- scanoss-1.43.1.dist-info/METADATA,sha256=DNoaMcUBfFRxK_HK8jRU_kGY-Beh-FGK314qzEUqhT4,6181
107
- scanoss-1.43.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
- scanoss-1.43.1.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
109
- scanoss-1.43.1.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
110
- scanoss-1.43.1.dist-info/RECORD,,
106
+ scanoss-1.45.0.dist-info/licenses/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
107
+ scanoss-1.45.0.dist-info/METADATA,sha256=xPca-0CXnZ6JXbrAuZVBY47jk-glQLmhNpATuinZhZk,6220
108
+ scanoss-1.45.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
109
+ scanoss-1.45.0.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
110
+ scanoss-1.45.0.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
111
+ scanoss-1.45.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5