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/__init__.py +1 -1
- scanoss/cli.py +630 -167
- scanoss/cyclonedx.py +41 -24
- scanoss/data/build_date.txt +1 -1
- scanoss/export/__init__.py +23 -0
- scanoss/export/dependency_track.py +222 -0
- scanoss/file_filters.py +1 -5
- scanoss/inspection/dependency_track/project_violation.py +443 -0
- scanoss/inspection/policy_check.py +54 -23
- scanoss/inspection/{component_summary.py → raw/component_summary.py} +3 -3
- scanoss/inspection/{copyleft.py → raw/copyleft.py} +63 -54
- scanoss/inspection/{license_summary.py → raw/license_summary.py} +5 -4
- scanoss/inspection/{inspect_base.py → raw/raw_base.py} +9 -6
- scanoss/inspection/{undeclared_component.py → raw/undeclared_component.py} +29 -25
- scanoss/services/dependency_track_service.py +131 -0
- {scanoss-1.29.0.dist-info → scanoss-1.31.0.dist-info}/METADATA +2 -1
- {scanoss-1.29.0.dist-info → scanoss-1.31.0.dist-info}/RECORD +21 -17
- {scanoss-1.29.0.dist-info → scanoss-1.31.0.dist-info}/WHEEL +0 -0
- {scanoss-1.29.0.dist-info → scanoss-1.31.0.dist-info}/entry_points.txt +0 -0
- {scanoss-1.29.0.dist-info → scanoss-1.31.0.dist-info}/licenses/LICENSE +0 -0
- {scanoss-1.29.0.dist-info → scanoss-1.31.0.dist-info}/top_level.txt +0 -0
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
scanoss/data/build_date.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
date:
|
|
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
|