scanoss 1.29.0__py3-none-any.whl → 1.31.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/cyclonedx.py CHANGED
@@ -28,6 +28,9 @@ import os.path
28
28
  import sys
29
29
  import uuid
30
30
 
31
+ from cyclonedx.schema import SchemaVersion
32
+ from cyclonedx.validation.json import JsonValidator
33
+
31
34
  from . import __version__
32
35
  from .scanossbase import ScanossBase
33
36
  from .spdxlite import SpdxLite
@@ -296,13 +299,13 @@ class CycloneDx(ScanossBase):
296
299
  """
297
300
  vuln_id = vuln.get('ID', '') or vuln.get('id', '')
298
301
  vuln_cve = vuln.get('CVE', '') or vuln.get('cve', '')
299
-
302
+
300
303
  # Skip CPE entries, use CVE if available
301
304
  if vuln_id.upper().startswith('CPE:') and vuln_cve:
302
305
  vuln_id = vuln_cve
303
-
306
+
304
307
  return vuln_id, vuln_cve
305
-
308
+
306
309
  def _create_vulnerability_entry(self, vuln_id: str, vuln: dict, vuln_cve: str, purl: str) -> dict:
307
310
  """
308
311
  Create a new vulnerability entry for CycloneDX format.
@@ -313,61 +316,56 @@ class CycloneDx(ScanossBase):
313
316
  'source': {
314
317
  'name': 'NVD' if vuln_source == 'nvd' else 'GitHub Advisories',
315
318
  'url': f'https://nvd.nist.gov/vuln/detail/{vuln_cve}'
316
- if vuln_source == 'nvd'
317
- else f'https://github.com/advisories/{vuln_id}'
319
+ if vuln_source == 'nvd'
320
+ else f'https://github.com/advisories/{vuln_id}',
318
321
  },
319
322
  'ratings': [{'severity': self._sev_lookup(vuln.get('severity', 'unknown').lower())}],
320
- 'affects': [{'ref': purl}]
323
+ 'affects': [{'ref': purl}],
321
324
  }
322
-
325
+
323
326
  def append_vulnerabilities(self, cdx_dict: dict, vulnerabilities_data: dict, purl: str) -> dict:
324
327
  """
325
328
  Append vulnerabilities to an existing CycloneDX dictionary
326
-
329
+
327
330
  Args:
328
331
  cdx_dict (dict): The existing CycloneDX dictionary
329
332
  vulnerabilities_data (dict): The vulnerabilities data from get_vulnerabilities_json
330
333
  purl (str): The PURL of the component these vulnerabilities affect
331
-
334
+
332
335
  Returns:
333
336
  dict: The updated CycloneDX dictionary with vulnerabilities appended
334
337
  """
335
338
  if not cdx_dict or not vulnerabilities_data:
336
339
  return cdx_dict
337
-
340
+
338
341
  if 'vulnerabilities' not in cdx_dict:
339
342
  cdx_dict['vulnerabilities'] = []
340
-
343
+
341
344
  # Extract vulnerabilities from the response
342
345
  vulns_list = vulnerabilities_data.get('purls', [])
343
346
  if not vulns_list:
344
347
  return cdx_dict
345
-
348
+
346
349
  vuln_items = vulns_list[0].get('vulnerabilities', [])
347
-
350
+
348
351
  for vuln in vuln_items:
349
352
  vuln_id, vuln_cve = self._normalize_vulnerability_id(vuln)
350
-
353
+
351
354
  # Skip empty IDs or CPE-only entries
352
355
  if not vuln_id or vuln_id.upper().startswith('CPE:'):
353
356
  continue
354
-
357
+
355
358
  # Check if vulnerability already exists
356
- existing_vuln = next(
357
- (v for v in cdx_dict['vulnerabilities'] if v.get('id') == vuln_id),
358
- None
359
- )
360
-
359
+ existing_vuln = next((v for v in cdx_dict['vulnerabilities'] if v.get('id') == vuln_id), None)
360
+
361
361
  if existing_vuln:
362
362
  # Add this PURL to the affects list if not already present
363
363
  if not any(ref.get('ref') == purl for ref in existing_vuln.get('affects', [])):
364
364
  existing_vuln['affects'].append({'ref': purl})
365
365
  else:
366
366
  # Create new vulnerability entry
367
- cdx_dict['vulnerabilities'].append(
368
- self._create_vulnerability_entry(vuln_id, vuln, vuln_cve, purl)
369
- )
370
-
367
+ cdx_dict['vulnerabilities'].append(self._create_vulnerability_entry(vuln_id, vuln, vuln_cve, purl))
368
+
371
369
  return cdx_dict
372
370
 
373
371
  @staticmethod
@@ -388,6 +386,25 @@ class CycloneDx(ScanossBase):
388
386
  'unknown': 'unknown',
389
387
  }.get(value, 'unknown')
390
388
 
389
+ def is_cyclonedx_json(self, json_string: str) -> bool:
390
+ """
391
+ Validate if the given JSON string is a valid CycloneDX JSON string
392
+ Args:
393
+ json_string (str): JSON string to validate
394
+ Returns:
395
+ bool: True if the JSON string is valid, False otherwise
396
+ """
397
+ try:
398
+ cdx_json_validator = JsonValidator(SchemaVersion.V1_6)
399
+ json_validation_errors = cdx_json_validator.validate_str(json_string)
400
+ if json_validation_errors:
401
+ self.print_stderr(f'ERROR: Problem parsing input JSON: {json_validation_errors}')
402
+ return False
403
+ return True
404
+ except Exception as e:
405
+ self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
406
+ return False
407
+
391
408
 
392
409
  #
393
410
  # End of CycloneDX Class
@@ -1 +1 @@
1
- date: 20250715073533, utime: 1752564933
1
+ date: 20250808112827, utime: 1754652507
@@ -0,0 +1,23 @@
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
+ """
@@ -0,0 +1,222 @@
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
+ import base64
26
+ import json
27
+ import traceback
28
+
29
+ import requests
30
+
31
+ from ..cyclonedx import CycloneDx
32
+ from ..scanossbase import ScanossBase
33
+ from ..services.dependency_track_service import DependencyTrackService
34
+ from ..utils.file import validate_json_file
35
+
36
+
37
+ def _build_payload(encoded_sbom: str, project_id, project_name, project_version) -> dict:
38
+ """
39
+ Build the API payload
40
+
41
+ Args:
42
+ encoded_sbom: Base64 encoded SBOM
43
+
44
+ Returns:
45
+ API payload dictionary
46
+ """
47
+ if project_id:
48
+ return {'project': project_id, 'bom': encoded_sbom}
49
+ else:
50
+ return {
51
+ 'projectName': project_name,
52
+ 'projectVersion': project_version,
53
+ 'autoCreate': True,
54
+ 'bom': encoded_sbom,
55
+ }
56
+
57
+
58
+ class DependencyTrackExporter(ScanossBase):
59
+ """
60
+ Class for exporting SBOM files to Dependency Track
61
+ """
62
+ def __init__( # noqa: PLR0913
63
+ self,
64
+ url: str = None,
65
+ apikey: str = None,
66
+ output: str = None,
67
+ debug: bool = False,
68
+ trace: bool = False,
69
+ quiet: bool = False
70
+ ):
71
+ """
72
+ Initialize DependencyTrackExporter
73
+
74
+ Args:
75
+ url: Dependency Track URL
76
+ apikey: Dependency Track API Key
77
+ output: File to store output response data (optional)
78
+ debug: Enable debug output
79
+ trace: Enable trace output
80
+ quiet: Enable quiet mode
81
+ """
82
+ super().__init__(debug=debug, trace=trace, quiet=quiet)
83
+ self.url = url.rstrip('/')
84
+ self.apikey = apikey
85
+ self.output = output
86
+ self.dt_service = DependencyTrackService(self.apikey, self.url, debug=debug, trace=trace, quiet=quiet)
87
+
88
+ def _read_and_validate_sbom(self, input_file: str) -> dict:
89
+ """
90
+ Read and validate the SBOM file
91
+
92
+ Args:
93
+ input_file: Path to the SBOM file
94
+
95
+ Returns:
96
+ Parsed SBOM content as dictionary
97
+
98
+ Raises:
99
+ ValueError: If the file doesn't exist or is invalid or not a valid CycloneDX SBOM
100
+ """
101
+ result = validate_json_file(input_file)
102
+ if not result.is_valid:
103
+ raise ValueError(f'Invalid JSON file: {result.error}')
104
+
105
+ cdx = CycloneDx(debug=self.debug)
106
+ if not cdx.is_cyclonedx_json(json.dumps(result.data)):
107
+ raise ValueError(f'Input file is not a valid CycloneDX SBOM: {input_file}')
108
+ return result.data
109
+
110
+ def _encode_sbom(self, sbom_content: dict) -> str:
111
+ """
112
+ Encode SBOM content to base64
113
+
114
+ Args:
115
+ sbom_content: SBOM dictionary
116
+
117
+ Returns:
118
+ Base64 encoded string
119
+ """
120
+ if not sbom_content:
121
+ self.print_stderr('Warning: Empty SBOM content')
122
+ json_str = json.dumps(sbom_content, separators=(',', ':'))
123
+ encoded = base64.b64encode(json_str.encode('utf-8')).decode('utf-8')
124
+ return encoded
125
+
126
+ def upload_sbom_file(self, input_file, project_id, project_name, project_version, output_file):
127
+ """
128
+ Uploads an SBOM file to the specified project with an
129
+ optional output file and processes the file content for validation.
130
+
131
+ Args:
132
+ input_file (str): The path to the SBOM file to be read and uploaded.
133
+ project_id (str): The unique identifier of the project to which the SBOM is being uploaded.
134
+ project_name (str): The name of the project to which the SBOM is being uploaded.
135
+ project_version (str): The version of the project to which the SBOM is being uploaded.
136
+ output_file (str): The path to save output related to the SBOM upload process.
137
+
138
+ Returns:
139
+ bool: Returns True if the SBOM file was uploaded successfully, False otherwise.
140
+
141
+ Raises:
142
+ ValueError: Raised if there are validation issues with the SBOM content.
143
+ """
144
+ try:
145
+ if not self.quiet:
146
+ self.print_stderr(f'Reading SBOM file: {input_file}')
147
+ sbom_content = self._read_and_validate_sbom(input_file)
148
+ return self.upload_sbom_contents(sbom_content, project_id, project_name, project_version, output_file)
149
+ except ValueError as e:
150
+ self.print_stderr(f'Validation error: {e}')
151
+ return False
152
+
153
+ def upload_sbom_contents(self, sbom_content: dict, project_id, project_name, project_version, output_file) -> bool:
154
+ """
155
+ Uploads an SBOM to a Dependency Track server.
156
+
157
+ Parameters:
158
+ sbom_content (dict): The SBOM content in dictionary format to be uploaded.
159
+ project_id: The unique identifier for the project.
160
+ project_name: The name of the project in Dependency Track.
161
+ project_version: The version of the project in Dependency Track.
162
+ output_file: The path to the file where the token and UUID data
163
+ should be written. If not provided, the data will be written to
164
+ standard output.
165
+
166
+ Returns:
167
+ bool: True if the upload is successful; False otherwise.
168
+
169
+ Raises:
170
+ ValueError: If the SBOM encoding process fails.
171
+ requests.exceptions.RequestException: If an error occurs during the HTTP request.
172
+ Exception: For any other unexpected error.
173
+ """
174
+ if not project_id and not (project_name and project_version):
175
+ self.print_stderr('Error: Missing project id or name and version.')
176
+ return False
177
+ output = self.output
178
+ if output_file:
179
+ output = output_file
180
+ try:
181
+ self.print_debug('Encoding SBOM to base64')
182
+ payload = _build_payload(self._encode_sbom(sbom_content), project_id, project_name, project_version)
183
+ url = f'{self.url}/api/v1/bom'
184
+ headers = {'Content-Type': 'application/json', 'X-Api-Key': self.apikey}
185
+ self.print_trace(f'URL: {url}, Headers: {headers}, Payload keys: {list(payload.keys())}')
186
+ self.print_msg('Uploading SBOM to Dependency Track...')
187
+ response = requests.put(url, json=payload, headers=headers)
188
+ response.raise_for_status()
189
+ # Treat any 2xx status as success
190
+ if (requests.codes.ok <= response.status_code < requests.codes.multiple_choices and
191
+ response.status_code != requests.codes.no_content):
192
+ self.print_msg('SBOM uploaded successfully')
193
+ try:
194
+ response_data = response.json()
195
+ token = ''
196
+ project_uuid = project_id
197
+ if 'token' in response_data:
198
+ token = response_data['token']
199
+ if project_name and project_version:
200
+ project_data = self.dt_service.get_project_by_name_version(project_name, project_version)
201
+ if project_data:
202
+ project_uuid = project_data.get("uuid", project_id)
203
+ token_json = json.dumps(
204
+ {"token": token, "project_uuid": project_uuid},
205
+ indent=2
206
+ )
207
+ self.print_to_file_or_stdout(token_json, output)
208
+ except json.JSONDecodeError:
209
+ pass
210
+ return True
211
+ else:
212
+ self.print_stderr(f'Upload failed with status code: {response.status_code}')
213
+ self.print_stderr(f'Response: {response.text}')
214
+ except ValueError as e:
215
+ self.print_stderr(f'DT SBOM Upload Validation error: {e}')
216
+ except requests.exceptions.RequestException as e:
217
+ self.print_stderr(f'DT API Request error: {e}')
218
+ except Exception as e:
219
+ self.print_stderr(f'Unexpected error: {e}')
220
+ if self.debug:
221
+ traceback.print_exc()
222
+ return False
scanoss/file_filters.py CHANGED
@@ -72,11 +72,7 @@ DEFAULT_SKIPPED_DIRS = {
72
72
  'htmlcov',
73
73
  '__pypackages__',
74
74
  'example',
75
- 'examples',
76
- 'docs',
77
- 'tests',
78
- 'doc',
79
- 'test',
75
+ 'examples'
80
76
  }
81
77
 
82
78
  DEFAULT_SKIPPED_DIRS_HFH = {