scanoss 1.28.3__py3-none-any.whl → 1.30.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 +99 -21
- scanoss/cyclonedx.py +45 -28
- scanoss/data/build_date.txt +1 -1
- scanoss/export/__init__.py +23 -0
- scanoss/export/dependency_track.py +221 -0
- {scanoss-1.28.3.dist-info → scanoss-1.30.0.dist-info}/METADATA +4 -3
- {scanoss-1.28.3.dist-info → scanoss-1.30.0.dist-info}/RECORD +12 -10
- {scanoss-1.28.3.dist-info → scanoss-1.30.0.dist-info}/WHEEL +0 -0
- {scanoss-1.28.3.dist-info → scanoss-1.30.0.dist-info}/entry_points.txt +0 -0
- {scanoss-1.28.3.dist-info → scanoss-1.30.0.dist-info}/licenses/LICENSE +0 -0
- {scanoss-1.28.3.dist-info → scanoss-1.30.0.dist-info}/top_level.txt +0 -0
scanoss/__init__.py
CHANGED
scanoss/cli.py
CHANGED
|
@@ -25,6 +25,7 @@ SPDX-License-Identifier: MIT
|
|
|
25
25
|
import argparse
|
|
26
26
|
import os
|
|
27
27
|
import sys
|
|
28
|
+
import traceback
|
|
28
29
|
from dataclasses import asdict
|
|
29
30
|
from pathlib import Path
|
|
30
31
|
from typing import List
|
|
@@ -32,6 +33,10 @@ from typing import List
|
|
|
32
33
|
import pypac
|
|
33
34
|
|
|
34
35
|
from scanoss.cryptography import Cryptography, create_cryptography_config_from_args
|
|
36
|
+
from scanoss.export.dependency_track import (
|
|
37
|
+
DependencyTrackExporter,
|
|
38
|
+
create_dependency_track_exporter_config_from_args,
|
|
39
|
+
)
|
|
35
40
|
from scanoss.inspection.component_summary import ComponentSummary
|
|
36
41
|
from scanoss.inspection.license_summary import LicenseSummary
|
|
37
42
|
from scanoss.scanners.container_scanner import (
|
|
@@ -553,13 +558,17 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
|
|
|
553
558
|
####### INSPECT: License Summary ######
|
|
554
559
|
# Inspect Sub-command: inspect license summary
|
|
555
560
|
p_license_summary = p_inspect_sub.add_parser(
|
|
556
|
-
'license-summary',
|
|
557
|
-
|
|
561
|
+
'license-summary',
|
|
562
|
+
aliases=['lic-summary', 'licsum'],
|
|
563
|
+
description='Get license summary',
|
|
564
|
+
help='Get detected license summary from scan results',
|
|
558
565
|
)
|
|
559
566
|
|
|
560
567
|
p_component_summary = p_inspect_sub.add_parser(
|
|
561
|
-
'component-summary',
|
|
562
|
-
|
|
568
|
+
'component-summary',
|
|
569
|
+
aliases=['comp-summary', 'compsum'],
|
|
570
|
+
description='Get component summary',
|
|
571
|
+
help='Get detected component summary from scan results',
|
|
563
572
|
)
|
|
564
573
|
|
|
565
574
|
####### INSPECT: Undeclared components ######
|
|
@@ -605,6 +614,36 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
|
|
|
605
614
|
|
|
606
615
|
########################################### END INSPECT SUBCOMMAND ###########################################
|
|
607
616
|
|
|
617
|
+
# Sub-command: export
|
|
618
|
+
p_export = subparsers.add_parser(
|
|
619
|
+
'export',
|
|
620
|
+
aliases=['exp'],
|
|
621
|
+
description=f'Export SBOM files to external platforms: {__version__}',
|
|
622
|
+
help='Export SBOM files to external platforms',
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
export_sub = p_export.add_subparsers(
|
|
626
|
+
title='Export Commands',
|
|
627
|
+
dest='subparsercmd',
|
|
628
|
+
description='export sub-commands',
|
|
629
|
+
help='export sub-commands',
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
# Export Sub-command: export dt (Dependency Track)
|
|
633
|
+
e_dt = export_sub.add_parser(
|
|
634
|
+
'dt',
|
|
635
|
+
aliases=['dependency-track'],
|
|
636
|
+
description='Export SBOM to Dependency Track',
|
|
637
|
+
help='Upload SBOM files to Dependency Track',
|
|
638
|
+
)
|
|
639
|
+
e_dt.add_argument('-i', '--input', type=str, required=True, help='Input SBOM file (CycloneDX JSON format)')
|
|
640
|
+
e_dt.add_argument('--dt-url', type=str, required=True, help='Dependency Track base URL')
|
|
641
|
+
e_dt.add_argument('--dt-apikey', type=str, required=True, help='Dependency Track API key')
|
|
642
|
+
e_dt.add_argument('--dt-projectid', type=str, help='Dependency Track project UUID')
|
|
643
|
+
e_dt.add_argument('--dt-projectname', type=str, help='Dependency Track project name')
|
|
644
|
+
e_dt.add_argument('--dt-projectversion', type=str, help='Dependency Track project version')
|
|
645
|
+
e_dt.set_defaults(func=export_dt)
|
|
646
|
+
|
|
608
647
|
# Sub-command: folder-scan
|
|
609
648
|
p_folder_scan = subparsers.add_parser(
|
|
610
649
|
'folder-scan',
|
|
@@ -858,6 +897,7 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
|
|
|
858
897
|
p_crypto_algorithms,
|
|
859
898
|
p_crypto_hints,
|
|
860
899
|
p_crypto_versions_in_range,
|
|
900
|
+
e_dt,
|
|
861
901
|
]:
|
|
862
902
|
p.add_argument('--debug', '-d', action='store_true', help='Enable debug messages')
|
|
863
903
|
p.add_argument('--trace', '-t', action='store_true', help='Enable trace messages, including API posts')
|
|
@@ -871,7 +911,8 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
|
|
|
871
911
|
parser.print_help() # No sub command subcommand, print general help
|
|
872
912
|
sys.exit(1)
|
|
873
913
|
elif (
|
|
874
|
-
args.subparser
|
|
914
|
+
args.subparser
|
|
915
|
+
in ('utils', 'ut', 'component', 'comp', 'inspect', 'insp', 'ins', 'crypto', 'cr', 'export', 'exp')
|
|
875
916
|
) and not args.subparsercmd:
|
|
876
917
|
parser.parse_args([args.subparser, '--help']) # Force utils helps to be displayed
|
|
877
918
|
sys.exit(1)
|
|
@@ -1304,6 +1345,7 @@ def convert(parser, args):
|
|
|
1304
1345
|
if not success:
|
|
1305
1346
|
sys.exit(1)
|
|
1306
1347
|
|
|
1348
|
+
|
|
1307
1349
|
################################ INSPECT handlers ################################
|
|
1308
1350
|
def inspect_copyleft(parser, args):
|
|
1309
1351
|
"""
|
|
@@ -1381,16 +1423,17 @@ def inspect_undeclared(parser, args):
|
|
|
1381
1423
|
status, _ = i_undeclared.run()
|
|
1382
1424
|
sys.exit(status)
|
|
1383
1425
|
|
|
1426
|
+
|
|
1384
1427
|
def inspect_license_summary(parser, args):
|
|
1385
1428
|
"""
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1429
|
+
Run the "inspect" sub-command
|
|
1430
|
+
Parameters
|
|
1431
|
+
----------
|
|
1432
|
+
parser: ArgumentParser
|
|
1433
|
+
command line parser object
|
|
1434
|
+
args: Namespace
|
|
1435
|
+
Parsed arguments
|
|
1436
|
+
"""
|
|
1394
1437
|
if args.input is None:
|
|
1395
1438
|
print_stderr('Please specify an input file to inspect')
|
|
1396
1439
|
parser.parse_args([args.subparser, args.subparsercmd, '-h'])
|
|
@@ -1412,16 +1455,17 @@ def inspect_license_summary(parser, args):
|
|
|
1412
1455
|
)
|
|
1413
1456
|
i_license_summary.run()
|
|
1414
1457
|
|
|
1458
|
+
|
|
1415
1459
|
def inspect_component_summary(parser, args):
|
|
1416
1460
|
"""
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1461
|
+
Run the "inspect" sub-command
|
|
1462
|
+
Parameters
|
|
1463
|
+
----------
|
|
1464
|
+
parser: ArgumentParser
|
|
1465
|
+
command line parser object
|
|
1466
|
+
args: Namespace
|
|
1467
|
+
Parsed arguments
|
|
1468
|
+
"""
|
|
1425
1469
|
if args.input is None:
|
|
1426
1470
|
print_stderr('Please specify an input file to inspect')
|
|
1427
1471
|
parser.parse_args([args.subparser, args.subparsercmd, '-h'])
|
|
@@ -1440,8 +1484,42 @@ def inspect_component_summary(parser, args):
|
|
|
1440
1484
|
)
|
|
1441
1485
|
i_component_summary.run()
|
|
1442
1486
|
|
|
1487
|
+
|
|
1443
1488
|
################################ End inspect handlers ################################
|
|
1444
1489
|
|
|
1490
|
+
|
|
1491
|
+
def export_dt(parser, args):
|
|
1492
|
+
"""
|
|
1493
|
+
Run the "export dt" sub-command
|
|
1494
|
+
Parameters
|
|
1495
|
+
----------
|
|
1496
|
+
parser: ArgumentParser
|
|
1497
|
+
command line parser object
|
|
1498
|
+
args: Namespace
|
|
1499
|
+
Parsed arguments
|
|
1500
|
+
"""
|
|
1501
|
+
|
|
1502
|
+
try:
|
|
1503
|
+
config = create_dependency_track_exporter_config_from_args(args)
|
|
1504
|
+
dt_exporter = DependencyTrackExporter(
|
|
1505
|
+
config=config,
|
|
1506
|
+
debug=args.debug,
|
|
1507
|
+
trace=args.trace,
|
|
1508
|
+
quiet=args.quiet,
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
success = dt_exporter.upload_sbom(args.input)
|
|
1512
|
+
|
|
1513
|
+
if not success:
|
|
1514
|
+
sys.exit(1)
|
|
1515
|
+
|
|
1516
|
+
except Exception as e:
|
|
1517
|
+
print_stderr(f'ERROR: {e}')
|
|
1518
|
+
if args.debug:
|
|
1519
|
+
traceback.print_exc()
|
|
1520
|
+
sys.exit(1)
|
|
1521
|
+
|
|
1522
|
+
|
|
1445
1523
|
def utils_certloc(*_):
|
|
1446
1524
|
"""
|
|
1447
1525
|
Run the "utils certloc" sub-command
|
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
|
|
@@ -48,10 +51,10 @@ class CycloneDx(ScanossBase):
|
|
|
48
51
|
self.debug = debug
|
|
49
52
|
self._spdx = SpdxLite(debug=debug)
|
|
50
53
|
|
|
51
|
-
def parse(self, data:
|
|
54
|
+
def parse(self, data: dict): # noqa: PLR0912, PLR0915
|
|
52
55
|
"""
|
|
53
56
|
Parse the given input (raw/plain) JSON string and return CycloneDX summary
|
|
54
|
-
:param data:
|
|
57
|
+
:param data: dict - JSON object
|
|
55
58
|
:return: CycloneDX dictionary, and vulnerability dictionary
|
|
56
59
|
"""
|
|
57
60
|
if not data:
|
|
@@ -170,12 +173,12 @@ class CycloneDx(ScanossBase):
|
|
|
170
173
|
success = self.produce_from_str(f.read(), output_file)
|
|
171
174
|
return success
|
|
172
175
|
|
|
173
|
-
def produce_from_json(self, data:
|
|
176
|
+
def produce_from_json(self, data: dict, output_file: str = None) -> tuple[bool, dict]: # noqa: PLR0912
|
|
174
177
|
"""
|
|
175
178
|
Produce the CycloneDX output from the raw scan results input data
|
|
176
179
|
|
|
177
180
|
Args:
|
|
178
|
-
data (
|
|
181
|
+
data (dict): JSON object
|
|
179
182
|
output_file (str, optional): Output file (optional). Defaults to None.
|
|
180
183
|
|
|
181
184
|
Returns:
|
|
@@ -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: 20250722103216, utime: 1753180336
|
|
@@ -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,221 @@
|
|
|
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
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
import requests
|
|
32
|
+
|
|
33
|
+
from scanoss.cyclonedx import CycloneDx
|
|
34
|
+
|
|
35
|
+
from ..scanossbase import ScanossBase
|
|
36
|
+
from ..utils.file import validate_json_file
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class DependencyTrackExporterConfig:
|
|
41
|
+
debug: bool = False
|
|
42
|
+
trace: bool = False
|
|
43
|
+
quiet: bool = False
|
|
44
|
+
dt_url: str = None
|
|
45
|
+
dt_apikey: str = None
|
|
46
|
+
dt_projectid: Optional[str] = None
|
|
47
|
+
dt_projectname: Optional[str] = None
|
|
48
|
+
dt_projectversion: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_dependency_track_exporter_config_from_args(args) -> DependencyTrackExporterConfig:
|
|
52
|
+
return DependencyTrackExporterConfig(
|
|
53
|
+
debug=getattr(args, 'debug', False),
|
|
54
|
+
trace=getattr(args, 'trace', False),
|
|
55
|
+
quiet=getattr(args, 'quiet', False),
|
|
56
|
+
dt_url=getattr(args, 'dt_url', None),
|
|
57
|
+
dt_apikey=getattr(args, 'dt_apikey', None),
|
|
58
|
+
dt_projectid=getattr(args, 'dt_projectid', None),
|
|
59
|
+
dt_projectname=getattr(args, 'dt_projectname', None),
|
|
60
|
+
dt_projectversion=getattr(args, 'dt_projectversion', None),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DependencyTrackExporter(ScanossBase):
|
|
65
|
+
"""
|
|
66
|
+
Class for exporting SBOM files to Dependency Track
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
config: DependencyTrackExporterConfig,
|
|
72
|
+
debug: bool = False,
|
|
73
|
+
trace: bool = False,
|
|
74
|
+
quiet: bool = False,
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
Initialize DependencyTrackExporter
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
config: Configuration parameters for the dependency track exporter
|
|
81
|
+
debug: Enable debug output
|
|
82
|
+
trace: Enable trace output
|
|
83
|
+
quiet: Enable quiet mode
|
|
84
|
+
"""
|
|
85
|
+
super().__init__(debug=debug, trace=trace, quiet=quiet)
|
|
86
|
+
|
|
87
|
+
self.dt_url = config.dt_url.rstrip('/')
|
|
88
|
+
self.dt_apikey = config.dt_apikey
|
|
89
|
+
self.dt_projectid = config.dt_projectid
|
|
90
|
+
self.dt_projectname = config.dt_projectname
|
|
91
|
+
self.dt_projectversion = config.dt_projectversion
|
|
92
|
+
|
|
93
|
+
self._validate_config()
|
|
94
|
+
|
|
95
|
+
def _validate_config(self):
|
|
96
|
+
"""
|
|
97
|
+
Validate that the configuration is valid.
|
|
98
|
+
"""
|
|
99
|
+
has_id = bool(self.dt_projectid)
|
|
100
|
+
has_name_version = bool(self.dt_projectname and self.dt_projectversion)
|
|
101
|
+
|
|
102
|
+
if not (has_id or has_name_version):
|
|
103
|
+
raise ValueError('Either --dt-projectid OR (--dt-projectname and --dt-projectversion) must be provided')
|
|
104
|
+
|
|
105
|
+
if has_id and has_name_version:
|
|
106
|
+
self.print_debug('Both DT project ID and name/version provided. Using project ID.')
|
|
107
|
+
|
|
108
|
+
def _read_and_validate_sbom(self, input_file: str) -> dict:
|
|
109
|
+
"""
|
|
110
|
+
Read and validate the SBOM file
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
input_file: Path to the SBOM file
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Parsed SBOM content as dictionary
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValueError: If file doesn't exist or is invalid or not a valid CycloneDX SBOM
|
|
120
|
+
"""
|
|
121
|
+
result = validate_json_file(input_file)
|
|
122
|
+
if not result.is_valid:
|
|
123
|
+
raise ValueError(f'Invalid JSON file: {result.error}')
|
|
124
|
+
|
|
125
|
+
cdx = CycloneDx(debug=self.debug)
|
|
126
|
+
if not cdx.is_cyclonedx_json(json.dumps(result.data)):
|
|
127
|
+
raise ValueError(f'Input file is not a valid CycloneDX SBOM: {input_file}')
|
|
128
|
+
|
|
129
|
+
return result.data
|
|
130
|
+
|
|
131
|
+
def _encode_sbom(self, sbom_content: dict) -> str:
|
|
132
|
+
"""
|
|
133
|
+
Encode SBOM content to base64
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
sbom_content: SBOM dictionary
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Base64 encoded string
|
|
140
|
+
"""
|
|
141
|
+
json_str = json.dumps(sbom_content, separators=(',', ':'))
|
|
142
|
+
encoded = base64.b64encode(json_str.encode('utf-8')).decode('utf-8')
|
|
143
|
+
return encoded
|
|
144
|
+
|
|
145
|
+
def _build_payload(self, encoded_sbom: str) -> dict:
|
|
146
|
+
"""
|
|
147
|
+
Build the API payload
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
encoded_sbom: Base64 encoded SBOM
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
API payload dictionary
|
|
154
|
+
"""
|
|
155
|
+
if self.dt_projectid:
|
|
156
|
+
return {'project': self.dt_projectid, 'bom': encoded_sbom}
|
|
157
|
+
else:
|
|
158
|
+
return {
|
|
159
|
+
'projectName': self.dt_projectname,
|
|
160
|
+
'projectVersion': self.dt_projectversion,
|
|
161
|
+
'autoCreate': True,
|
|
162
|
+
'bom': encoded_sbom,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def upload_sbom(self, input_file: str) -> bool:
|
|
166
|
+
"""
|
|
167
|
+
Upload SBOM file to Dependency Track
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
input_file: Path to the SBOM file
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if successful, False otherwise
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
self.print_stderr(f'Reading SBOM file: {input_file}')
|
|
177
|
+
sbom_content = self._read_and_validate_sbom(input_file)
|
|
178
|
+
|
|
179
|
+
self.print_debug('Encoding SBOM to base64')
|
|
180
|
+
encoded_sbom = self._encode_sbom(sbom_content)
|
|
181
|
+
|
|
182
|
+
payload = self._build_payload(encoded_sbom)
|
|
183
|
+
|
|
184
|
+
url = f'{self.dt_url}/api/v1/bom'
|
|
185
|
+
headers = {'Content-Type': 'application/json', 'X-Api-Key': self.dt_apikey}
|
|
186
|
+
|
|
187
|
+
if self.trace:
|
|
188
|
+
self.print_trace(f'URL: {url}')
|
|
189
|
+
self.print_trace(f'Headers: {headers}')
|
|
190
|
+
self.print_trace(f'Payload keys: {list(payload.keys())}')
|
|
191
|
+
|
|
192
|
+
self.print_msg('Uploading SBOM to Dependency Track...')
|
|
193
|
+
response = requests.put(url, json=payload, headers=headers)
|
|
194
|
+
|
|
195
|
+
if response.status_code in [200, 201]:
|
|
196
|
+
self.print_stderr('SBOM uploaded successfully')
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
response_data = response.json()
|
|
200
|
+
if 'token' in response_data:
|
|
201
|
+
self.print_stderr(f'Upload token: {response_data["token"]}')
|
|
202
|
+
except json.JSONDecodeError:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
return True
|
|
206
|
+
else:
|
|
207
|
+
self.print_stderr(f'Upload failed with status code: {response.status_code}')
|
|
208
|
+
self.print_stderr(f'Response: {response.text}')
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
except ValueError as e:
|
|
212
|
+
self.print_stderr(f'Validation error: {e}')
|
|
213
|
+
return False
|
|
214
|
+
except requests.exceptions.RequestException as e:
|
|
215
|
+
self.print_stderr(f'Request error: {e}')
|
|
216
|
+
return False
|
|
217
|
+
except Exception as e:
|
|
218
|
+
self.print_stderr(f'Unexpected error: {e}')
|
|
219
|
+
if self.debug:
|
|
220
|
+
traceback.print_exc()
|
|
221
|
+
return False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scanoss
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30.0
|
|
4
4
|
Summary: Simple Python library to leverage the SCANOSS APIs
|
|
5
5
|
Home-page: https://scanoss.com
|
|
6
6
|
Author: SCANOSS
|
|
@@ -13,7 +13,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Development Status :: 5 - Production/Stable
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Requires-Python: >=3.
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: requests
|
|
@@ -30,6 +30,7 @@ Requires-Dist: packageurl-python
|
|
|
30
30
|
Requires-Dist: pathspec
|
|
31
31
|
Requires-Dist: jsonschema
|
|
32
32
|
Requires-Dist: crc
|
|
33
|
+
Requires-Dist: cyclonedx-python-lib[validation]
|
|
33
34
|
Provides-Extra: fast-winnowing
|
|
34
35
|
Requires-Dist: scanoss_winnowing>=0.5.0; extra == "fast-winnowing"
|
|
35
36
|
Dynamic: license-file
|
|
@@ -174,7 +175,7 @@ if __name__ == "__main__":
|
|
|
174
175
|
```
|
|
175
176
|
|
|
176
177
|
## Requirements
|
|
177
|
-
Python 3.
|
|
178
|
+
Python 3.9 or higher.
|
|
178
179
|
|
|
179
180
|
## Source code
|
|
180
181
|
The source for this package can be found [here](https://github.com/scanoss/scanoss.py).
|
|
@@ -4,13 +4,13 @@ protoc_gen_swagger/options/annotations_pb2.py,sha256=b25EDD6gssUWnFby9gxgcpLIROT
|
|
|
4
4
|
protoc_gen_swagger/options/annotations_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
|
|
5
5
|
protoc_gen_swagger/options/openapiv2_pb2.py,sha256=vYElGp8E1vGHszvWqX97zNG9GFJ7u2QcdK9ouq0XdyI,14939
|
|
6
6
|
protoc_gen_swagger/options/openapiv2_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
|
|
7
|
-
scanoss/__init__.py,sha256=
|
|
8
|
-
scanoss/cli.py,sha256=
|
|
7
|
+
scanoss/__init__.py,sha256=Y6EsxqgHsge72r1bbXior15XkUCks6uDBkv4qD_q4OQ,1146
|
|
8
|
+
scanoss/cli.py,sha256=RXC-4CnjPFKlO2fi4OV-I4fQZXvQkO9MyzZVzbe9N4g,74777
|
|
9
9
|
scanoss/components.py,sha256=b0R9DdKuXqyQiw5nZZwjQ6NJXBr1U9gyx1RI2FP9ozA,14511
|
|
10
10
|
scanoss/constants.py,sha256=On8mQ-8ardVMHSJ7WOJqeTvGXIOWPLCgUanjE7Wk-wE,351
|
|
11
11
|
scanoss/cryptography.py,sha256=oj5HHgJk1e31dzQfB-5sIVmQVcUJMsP5DUPyP9QpPgQ,9806
|
|
12
12
|
scanoss/csvoutput.py,sha256=qNKRwcChSkgIwLm00kZiVX6iHVQUF4Apl-sMbzJ5Taw,10192
|
|
13
|
-
scanoss/cyclonedx.py,sha256=
|
|
13
|
+
scanoss/cyclonedx.py,sha256=JeAeuj2KYaN72v_08j7b3ZyVOhjXhOHOW7DE3Byp0Wk,16986
|
|
14
14
|
scanoss/file_filters.py,sha256=2DzyvSVR7We7U36UurtJj3cdQturUjDl8j3OIqmv4Pg,20638
|
|
15
15
|
scanoss/filecount.py,sha256=RZjKQ6M5P_RQg0_PMD2tsRe5Z8f98ke0sxYVjPDN8iQ,6538
|
|
16
16
|
scanoss/results.py,sha256=47ZXXuU2sDjYa5vhtbWTmikit9jHhA0rsYKwkvZFI5w,9252
|
|
@@ -57,10 +57,12 @@ scanoss/api/vulnerabilities/__init__.py,sha256=IFrDk_DTJgKSZmmU-nuLXuq_s8sQZlrSC
|
|
|
57
57
|
scanoss/api/vulnerabilities/v2/__init__.py,sha256=IFrDk_DTJgKSZmmU-nuLXuq_s8sQZlrSCHhIDMJT4r0,1122
|
|
58
58
|
scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py,sha256=CFhF80av8tenGvn9AIsGEtRJPuV2dC_syA5JLZb2lDw,5464
|
|
59
59
|
scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py,sha256=HlS4k4Zmx6RIAqaO9I96jD-eyF5yU6Xx04pVm7pdqOg,6864
|
|
60
|
-
scanoss/data/build_date.txt,sha256=
|
|
60
|
+
scanoss/data/build_date.txt,sha256=0rK9m6BDqN8Fl_4DclWg16-g7JdJX0fGtb4q381y7Dg,40
|
|
61
61
|
scanoss/data/scanoss-settings-schema.json,sha256=ClkRYAkjAN0Sk704G8BE_Ok006oQ6YnIGmX84CF8h9w,8798
|
|
62
62
|
scanoss/data/spdx-exceptions.json,sha256=s7UTYxC7jqQXr11YBlIWYCNwN6lRDFTR33Y8rpN_dA4,17953
|
|
63
63
|
scanoss/data/spdx-licenses.json,sha256=A6Z0q82gaTLtnopBfzeIVZjJFxkdRW1g2TuumQc-lII,228794
|
|
64
|
+
scanoss/export/__init__.py,sha256=D4C0lWLuNp8k_BjQZEc07WZcUgAvriVwQWOk063b0ZU,1122
|
|
65
|
+
scanoss/export/dependency_track.py,sha256=8jnvNYX8Phit_aMFG43FkuxUvGby_nqW3KwSUfH8OSQ,7458
|
|
64
66
|
scanoss/inspection/__init__.py,sha256=D4C0lWLuNp8k_BjQZEc07WZcUgAvriVwQWOk063b0ZU,1122
|
|
65
67
|
scanoss/inspection/component_summary.py,sha256=h1l3rF6NnoK0wMkS4ib6rDfcza2aqunyoMDbN2lw2G4,4049
|
|
66
68
|
scanoss/inspection/copyleft.py,sha256=ZSA97Vc3o06e66r4SCxwLKjGZOjv6lo92sWbvpzKHvo,9237
|
|
@@ -79,9 +81,9 @@ scanoss/utils/abstract_presenter.py,sha256=teiDTxBj5jBMCk2T8i4l1BJPf_u4zBLWrtCTF
|
|
|
79
81
|
scanoss/utils/crc64.py,sha256=TMrwQimSdE6imhFOUL7oAG6Kxu-8qMpGWMuMg8QpSVs,3169
|
|
80
82
|
scanoss/utils/file.py,sha256=62cA9a17TU9ZvfA3FY5HY4-QOajJeSrc8S6xLA_f-3M,2980
|
|
81
83
|
scanoss/utils/simhash.py,sha256=6iu8DOcecPAY36SZjCOzrrLMT9oIE7-gI6QuYwUQ7B0,5793
|
|
82
|
-
scanoss-1.
|
|
83
|
-
scanoss-1.
|
|
84
|
-
scanoss-1.
|
|
85
|
-
scanoss-1.
|
|
86
|
-
scanoss-1.
|
|
87
|
-
scanoss-1.
|
|
84
|
+
scanoss-1.30.0.dist-info/licenses/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
|
|
85
|
+
scanoss-1.30.0.dist-info/METADATA,sha256=usVjgvKzC-kJAJIfZBzoj6KDuoOUm8XaHV3Jo-UYe_c,6108
|
|
86
|
+
scanoss-1.30.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
87
|
+
scanoss-1.30.0.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
|
|
88
|
+
scanoss-1.30.0.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
|
|
89
|
+
scanoss-1.30.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|