scanoss 1.20.5__py3-none-any.whl → 1.22.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.
- protoc_gen_swagger/options/annotations_pb2.py +9 -12
- protoc_gen_swagger/options/annotations_pb2_grpc.py +1 -1
- protoc_gen_swagger/options/openapiv2_pb2.py +96 -98
- protoc_gen_swagger/options/openapiv2_pb2_grpc.py +1 -1
- scanoss/__init__.py +1 -1
- scanoss/api/common/v2/scanoss_common_pb2.py +20 -18
- scanoss/api/common/v2/scanoss_common_pb2_grpc.py +1 -1
- scanoss/api/components/v2/scanoss_components_pb2.py +38 -48
- scanoss/api/components/v2/scanoss_components_pb2_grpc.py +96 -142
- scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py +42 -22
- scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py +185 -75
- scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +32 -30
- scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +83 -75
- scanoss/api/provenance/v2/scanoss_provenance_pb2.py +20 -21
- scanoss/api/provenance/v2/scanoss_provenance_pb2_grpc.py +1 -1
- scanoss/api/scanning/v2/scanoss_scanning_pb2.py +20 -10
- scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py +70 -40
- scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py +18 -22
- scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py +49 -71
- scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py +27 -37
- scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py +72 -109
- scanoss/cli.py +417 -74
- scanoss/components.py +5 -3
- scanoss/constants.py +12 -0
- scanoss/data/build_date.txt +1 -1
- scanoss/file_filters.py +272 -57
- scanoss/results.py +92 -109
- scanoss/scanner.py +25 -20
- scanoss/scanners/__init__.py +23 -0
- scanoss/scanners/container_scanner.py +474 -0
- scanoss/scanners/folder_hasher.py +302 -0
- scanoss/scanners/scanner_config.py +73 -0
- scanoss/scanners/scanner_hfh.py +172 -0
- scanoss/scanoss_settings.py +9 -5
- scanoss/scanossapi.py +29 -7
- scanoss/scanossbase.py +9 -3
- scanoss/scanossgrpc.py +145 -13
- scanoss/threadedscanning.py +6 -6
- scanoss/utils/abstract_presenter.py +103 -0
- scanoss/utils/crc64.py +96 -0
- scanoss/utils/simhash.py +198 -0
- {scanoss-1.20.5.dist-info → scanoss-1.22.0.dist-info}/METADATA +4 -2
- scanoss-1.22.0.dist-info/RECORD +83 -0
- {scanoss-1.20.5.dist-info → scanoss-1.22.0.dist-info}/WHEEL +1 -1
- scanoss-1.20.5.dist-info/RECORD +0 -74
- {scanoss-1.20.5.dist-info → scanoss-1.22.0.dist-info}/entry_points.txt +0 -0
- {scanoss-1.20.5.dist-info → scanoss-1.22.0.dist-info/licenses}/LICENSE +0 -0
- {scanoss-1.20.5.dist-info → scanoss-1.22.0.dist-info}/top_level.txt +0 -0
scanoss/scanossbase.py
CHANGED
|
@@ -80,20 +80,26 @@ class ScanossBase:
|
|
|
80
80
|
**kwargs,
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
def print_to_file_or_stdout(self,
|
|
83
|
+
def print_to_file_or_stdout(self, content: str, file: str = None):
|
|
84
84
|
"""
|
|
85
85
|
Print message to file if provided or stdout
|
|
86
86
|
"""
|
|
87
|
+
if not content:
|
|
88
|
+
return
|
|
89
|
+
|
|
87
90
|
if file:
|
|
88
91
|
with open(file, 'w') as f:
|
|
89
|
-
f.write(
|
|
92
|
+
f.write(content)
|
|
90
93
|
else:
|
|
91
|
-
self.print_stdout(
|
|
94
|
+
self.print_stdout(content)
|
|
92
95
|
|
|
93
96
|
def print_to_file_or_stderr(self, msg: str, file: str = None):
|
|
94
97
|
"""
|
|
95
98
|
Print message to file if provided or stderr
|
|
96
99
|
"""
|
|
100
|
+
if not msg:
|
|
101
|
+
return
|
|
102
|
+
|
|
97
103
|
if file:
|
|
98
104
|
with open(file, 'w') as f:
|
|
99
105
|
f.write(msg)
|
scanoss/scanossgrpc.py
CHANGED
|
@@ -26,6 +26,9 @@ import concurrent.futures
|
|
|
26
26
|
import json
|
|
27
27
|
import os
|
|
28
28
|
import uuid
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from enum import IntEnum
|
|
31
|
+
from typing import Dict, Optional
|
|
29
32
|
from urllib.parse import urlparse
|
|
30
33
|
|
|
31
34
|
import grpc
|
|
@@ -33,6 +36,10 @@ from google.protobuf.json_format import MessageToDict, ParseDict
|
|
|
33
36
|
from pypac.parser import PACFile
|
|
34
37
|
from pypac.resolver import ProxyResolver
|
|
35
38
|
|
|
39
|
+
from scanoss.api.provenance.v2.scanoss_provenance_pb2_grpc import ProvenanceStub
|
|
40
|
+
from scanoss.api.scanning.v2.scanoss_scanning_pb2_grpc import ScanningStub
|
|
41
|
+
from scanoss.constants import DEFAULT_TIMEOUT
|
|
42
|
+
|
|
36
43
|
from . import __version__
|
|
37
44
|
from .api.common.v2.scanoss_common_pb2 import (
|
|
38
45
|
EchoRequest,
|
|
@@ -53,7 +60,7 @@ from .api.cryptography.v2.scanoss_cryptography_pb2_grpc import CryptographyStub
|
|
|
53
60
|
from .api.dependencies.v2.scanoss_dependencies_pb2 import DependencyRequest
|
|
54
61
|
from .api.dependencies.v2.scanoss_dependencies_pb2_grpc import DependenciesStub
|
|
55
62
|
from .api.provenance.v2.scanoss_provenance_pb2 import ProvenanceResponse
|
|
56
|
-
from .api.
|
|
63
|
+
from .api.scanning.v2.scanoss_scanning_pb2 import HFHRequest
|
|
57
64
|
from .api.semgrep.v2.scanoss_semgrep_pb2 import SemgrepResponse
|
|
58
65
|
from .api.semgrep.v2.scanoss_semgrep_pb2_grpc import SemgrepStub
|
|
59
66
|
from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2 import VulnerabilityResponse
|
|
@@ -68,12 +75,29 @@ SCANOSS_API_KEY = os.environ.get('SCANOSS_API_KEY') if os.environ.get('SCANOSS_A
|
|
|
68
75
|
MAX_CONCURRENT_REQUESTS = 5
|
|
69
76
|
|
|
70
77
|
|
|
78
|
+
class ScanossGrpcError(Exception):
|
|
79
|
+
"""
|
|
80
|
+
Custom exception for SCANOSS gRPC errors
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ScanossGrpcStatusCode(IntEnum):
|
|
87
|
+
"""Status codes for SCANOSS gRPC responses"""
|
|
88
|
+
|
|
89
|
+
SUCCESS = 1
|
|
90
|
+
SUCCESS_WITH_WARNINGS = 2
|
|
91
|
+
FAILED_WITH_WARNINGS = 3
|
|
92
|
+
FAILED = 4
|
|
93
|
+
|
|
94
|
+
|
|
71
95
|
class ScanossGrpc(ScanossBase):
|
|
72
96
|
"""
|
|
73
97
|
Client for gRPC functionality
|
|
74
98
|
"""
|
|
75
99
|
|
|
76
|
-
def __init__( # noqa: PLR0913
|
|
100
|
+
def __init__( # noqa: PLR0913, PLR0915
|
|
77
101
|
self,
|
|
78
102
|
url: str = None,
|
|
79
103
|
debug: bool = False,
|
|
@@ -86,6 +110,7 @@ class ScanossGrpc(ScanossBase):
|
|
|
86
110
|
proxy: str = None,
|
|
87
111
|
grpc_proxy: str = None,
|
|
88
112
|
pac: PACFile = None,
|
|
113
|
+
req_headers: dict = None,
|
|
89
114
|
):
|
|
90
115
|
"""
|
|
91
116
|
|
|
@@ -103,22 +128,28 @@ class ScanossGrpc(ScanossBase):
|
|
|
103
128
|
grpc_proxy='http://<ip>:<port>'
|
|
104
129
|
"""
|
|
105
130
|
super().__init__(debug, trace, quiet)
|
|
106
|
-
self.url = url
|
|
131
|
+
self.url = url
|
|
107
132
|
self.api_key = api_key if api_key else SCANOSS_API_KEY
|
|
108
|
-
if self.api_key and not url and not os.environ.get('SCANOSS_GRPC_URL'):
|
|
109
|
-
self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium
|
|
110
|
-
self.url = self.url.lower()
|
|
111
|
-
self.orig_url = self.url # Used for proxy lookup
|
|
112
133
|
self.timeout = timeout
|
|
113
134
|
self.proxy = proxy
|
|
114
135
|
self.grpc_proxy = grpc_proxy
|
|
115
136
|
self.pac = pac
|
|
137
|
+
self.req_headers = req_headers
|
|
116
138
|
self.metadata = []
|
|
139
|
+
|
|
117
140
|
if self.api_key:
|
|
118
141
|
self.metadata.append(('x-api-key', api_key)) # Set API key if we have one
|
|
119
142
|
if ver_details:
|
|
120
143
|
self.metadata.append(('x-scanoss-client', ver_details))
|
|
121
144
|
self.metadata.append(('user-agent', f'scanoss-py/{__version__}'))
|
|
145
|
+
self.load_generic_headers()
|
|
146
|
+
|
|
147
|
+
self.url = url if url else SCANOSS_GRPC_URL
|
|
148
|
+
if self.api_key and not url and not os.environ.get('SCANOSS_GRPC_URL'):
|
|
149
|
+
self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium
|
|
150
|
+
self.url = self.url.lower()
|
|
151
|
+
self.orig_url = self.url # Used for proxy lookup
|
|
152
|
+
|
|
122
153
|
secure = True if self.url.startswith('https:') else False # Is it a secure connection?
|
|
123
154
|
if self.url.startswith('http'):
|
|
124
155
|
u = urlparse(self.url)
|
|
@@ -139,6 +170,7 @@ class ScanossGrpc(ScanossBase):
|
|
|
139
170
|
self.semgrep_stub = SemgrepStub(grpc.insecure_channel(self.url))
|
|
140
171
|
self.vuln_stub = VulnerabilitiesStub(grpc.insecure_channel(self.url))
|
|
141
172
|
self.provenance_stub = ProvenanceStub(grpc.insecure_channel(self.url))
|
|
173
|
+
self.scanning_stub = ScanningStub(grpc.insecure_channel(self.url))
|
|
142
174
|
else:
|
|
143
175
|
if ca_cert is not None:
|
|
144
176
|
credentials = grpc.ssl_channel_credentials(cert_data) # secure with specified certificate
|
|
@@ -150,6 +182,7 @@ class ScanossGrpc(ScanossBase):
|
|
|
150
182
|
self.semgrep_stub = SemgrepStub(grpc.secure_channel(self.url, credentials))
|
|
151
183
|
self.vuln_stub = VulnerabilitiesStub(grpc.secure_channel(self.url, credentials))
|
|
152
184
|
self.provenance_stub = ProvenanceStub(grpc.secure_channel(self.url, credentials))
|
|
185
|
+
self.scanning_stub = ScanningStub(grpc.secure_channel(self.url, credentials))
|
|
153
186
|
|
|
154
187
|
@classmethod
|
|
155
188
|
def _load_cert(cls, cert_file: str) -> bytes:
|
|
@@ -429,6 +462,62 @@ class ScanossGrpc(ScanossBase):
|
|
|
429
462
|
return resp_dict
|
|
430
463
|
return None
|
|
431
464
|
|
|
465
|
+
def folder_hash_scan(self, request: Dict) -> Dict:
|
|
466
|
+
"""
|
|
467
|
+
Client function to call the rpc for Folder Hashing Scan
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
request (Dict): Folder Hash Request
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
Dict: Folder Hash Response
|
|
474
|
+
"""
|
|
475
|
+
return self._call_rpc(
|
|
476
|
+
self.scanning_stub.FolderHashScan,
|
|
477
|
+
request,
|
|
478
|
+
HFHRequest,
|
|
479
|
+
'Sending folder hash scan data (rqId: {rqId})...',
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def _call_rpc(self, rpc_method, request_input, request_type, debug_msg: Optional[str] = None) -> dict:
|
|
483
|
+
"""
|
|
484
|
+
Call a gRPC method and return the response as a dictionary
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
rpc_method (): The gRPC stub method
|
|
488
|
+
request_input (): Either a dict or a gRPC request object.
|
|
489
|
+
request_type (): The type of the gRPC request object.
|
|
490
|
+
debug_msg (str, optional): Debug message template that can include {rqId} placeholder.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
dict: The parsed gRPC response as a dictionary, or None if an error occurred.
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
request_id = str(uuid.uuid4())
|
|
497
|
+
|
|
498
|
+
if isinstance(request_input, dict):
|
|
499
|
+
request_obj = ParseDict(request_input, request_type())
|
|
500
|
+
else:
|
|
501
|
+
request_obj = request_input
|
|
502
|
+
|
|
503
|
+
metadata = self.metadata[:] + [('x-request-id', request_id)]
|
|
504
|
+
|
|
505
|
+
self.print_debug(debug_msg.format(rqId=request_id))
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
resp = rpc_method(request_obj, metadata=metadata, timeout=self.timeout)
|
|
509
|
+
except grpc.RpcError as e:
|
|
510
|
+
raise ScanossGrpcError(
|
|
511
|
+
f'{e.__class__.__name__} while sending gRPC message (rqId: {request_id}): {e.details()}'
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
if resp and not self._check_status_response(resp.status, request_id):
|
|
515
|
+
raise ScanossGrpcError(f'Unsuccessful status response (rqId: {request_id}).')
|
|
516
|
+
|
|
517
|
+
resp_dict = MessageToDict(resp, preserving_proto_field_name=True)
|
|
518
|
+
resp_dict.pop('status', None)
|
|
519
|
+
return resp_dict
|
|
520
|
+
|
|
432
521
|
def _check_status_response(self, status_response: StatusResponse, request_id: str = None) -> bool:
|
|
433
522
|
"""
|
|
434
523
|
Check the response object to see if the command was successful or not
|
|
@@ -436,21 +525,18 @@ class ScanossGrpc(ScanossBase):
|
|
|
436
525
|
:return: True if successful, False otherwise
|
|
437
526
|
"""
|
|
438
527
|
|
|
439
|
-
SUCCEDED_WITH_WARNINGS_STATUS_CODE = 2
|
|
440
|
-
FAILED_STATUS_CODE = 3
|
|
441
|
-
|
|
442
528
|
if not status_response:
|
|
443
529
|
self.print_stderr(f'Warning: No status response supplied (rqId: {request_id}). Assuming it was ok.')
|
|
444
530
|
return True
|
|
445
531
|
self.print_debug(f'Checking response status (rqId: {request_id}): {status_response}')
|
|
446
532
|
status_code: StatusCode = status_response.status
|
|
447
|
-
if status_code >
|
|
533
|
+
if status_code > ScanossGrpcStatusCode.SUCCESS:
|
|
448
534
|
ret_val = False # default to failed
|
|
449
535
|
msg = 'Unsuccessful'
|
|
450
|
-
if status_code ==
|
|
536
|
+
if status_code == ScanossGrpcStatusCode.SUCCESS_WITH_WARNINGS:
|
|
451
537
|
msg = 'Succeeded with warnings'
|
|
452
538
|
ret_val = True # No need to fail as it succeeded with warnings
|
|
453
|
-
elif status_code ==
|
|
539
|
+
elif status_code == ScanossGrpcStatusCode.FAILED_WITH_WARNINGS:
|
|
454
540
|
msg = 'Failed with warnings'
|
|
455
541
|
self.print_stderr(f'{msg} (rqId: {request_id} - status: {status_code}): {status_response.message}')
|
|
456
542
|
return ret_val
|
|
@@ -507,7 +593,53 @@ class ScanossGrpc(ScanossBase):
|
|
|
507
593
|
return resp_dict
|
|
508
594
|
return None
|
|
509
595
|
|
|
596
|
+
def load_generic_headers(self):
|
|
597
|
+
"""
|
|
598
|
+
Adds custom headers from req_headers to metadata.
|
|
599
|
+
|
|
600
|
+
If x-api-key is present and no URL is configured (directly or via
|
|
601
|
+
environment), sets URL to the premium endpoint (DEFAULT_URL2).
|
|
602
|
+
"""
|
|
603
|
+
if self.req_headers: # Load generic headers
|
|
604
|
+
for key, value in self.req_headers.items():
|
|
605
|
+
if key == 'x-api-key': # Set premium URL if x-api-key header is set
|
|
606
|
+
if not self.url and not os.environ.get('SCANOSS_GRPC_URL'):
|
|
607
|
+
self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium
|
|
608
|
+
self.api_key = value
|
|
609
|
+
self.metadata.append((key, value))
|
|
610
|
+
|
|
510
611
|
|
|
511
612
|
#
|
|
512
613
|
# End of ScanossGrpc Class
|
|
513
614
|
#
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@dataclass
|
|
618
|
+
class GrpcConfig:
|
|
619
|
+
url: str = DEFAULT_URL
|
|
620
|
+
api_key: Optional[str] = SCANOSS_API_KEY
|
|
621
|
+
debug: Optional[bool] = False
|
|
622
|
+
trace: Optional[bool] = False
|
|
623
|
+
quiet: Optional[bool] = False
|
|
624
|
+
ver_details: Optional[str] = None
|
|
625
|
+
ca_cert: Optional[str] = None
|
|
626
|
+
pac: Optional[PACFile] = None
|
|
627
|
+
timeout: Optional[int] = DEFAULT_TIMEOUT
|
|
628
|
+
proxy: Optional[str] = None
|
|
629
|
+
grpc_proxy: Optional[str] = None
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def create_grpc_config_from_args(args) -> GrpcConfig:
|
|
633
|
+
return GrpcConfig(
|
|
634
|
+
url=getattr(args, 'api2url', DEFAULT_URL),
|
|
635
|
+
api_key=getattr(args, 'key', SCANOSS_API_KEY),
|
|
636
|
+
debug=getattr(args, 'debug', False),
|
|
637
|
+
trace=getattr(args, 'trace', False),
|
|
638
|
+
quiet=getattr(args, 'quiet', False),
|
|
639
|
+
ver_details=getattr(args, 'ver_details', None),
|
|
640
|
+
ca_cert=getattr(args, 'ca_cert', None),
|
|
641
|
+
pac=getattr(args, 'pac', None),
|
|
642
|
+
timeout=getattr(args, 'timeout', DEFAULT_TIMEOUT),
|
|
643
|
+
proxy=getattr(args, 'proxy', None),
|
|
644
|
+
grpc_proxy=getattr(args, 'grpc_proxy', None),
|
|
645
|
+
)
|
scanoss/threadedscanning.py
CHANGED
|
@@ -23,13 +23,13 @@ SPDX-License-Identifier: MIT
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
import os
|
|
26
|
+
import queue
|
|
26
27
|
import sys
|
|
27
28
|
import threading
|
|
28
|
-
import queue
|
|
29
29
|
import time
|
|
30
|
-
|
|
31
|
-
from typing import Dict, List
|
|
32
30
|
from dataclasses import dataclass
|
|
31
|
+
from typing import Dict, List
|
|
32
|
+
|
|
33
33
|
from progress.bar import Bar
|
|
34
34
|
|
|
35
35
|
from .scanossapi import ScanossApi
|
|
@@ -49,8 +49,6 @@ class ThreadedScanning(ScanossBase):
|
|
|
49
49
|
Multiple threads pull messages off this queue, process the request and put the results into an output queue
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
inputs: queue.Queue = queue.Queue()
|
|
53
|
-
output: queue.Queue = queue.Queue()
|
|
54
52
|
bar: Bar = None
|
|
55
53
|
|
|
56
54
|
def __init__(
|
|
@@ -65,6 +63,8 @@ class ThreadedScanning(ScanossBase):
|
|
|
65
63
|
:param nb_threads: Number of thread to run (default 5)
|
|
66
64
|
"""
|
|
67
65
|
super().__init__(debug, trace, quiet)
|
|
66
|
+
self.inputs = queue.Queue()
|
|
67
|
+
self.output = queue.Queue()
|
|
68
68
|
self.scanapi = scanapi
|
|
69
69
|
self.nb_threads = nb_threads
|
|
70
70
|
self._isatty = sys.stderr.isatty()
|
|
@@ -134,7 +134,7 @@ class ThreadedScanning(ScanossBase):
|
|
|
134
134
|
:param wfp: WFP to add to queue
|
|
135
135
|
"""
|
|
136
136
|
if wfp is None or wfp == '':
|
|
137
|
-
self.print_stderr(
|
|
137
|
+
self.print_stderr('Warning: empty WFP. Skipping from scan...')
|
|
138
138
|
else:
|
|
139
139
|
self.inputs.put(wfp)
|
|
140
140
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from scanoss.scanossbase import ScanossBase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AbstractPresenter(ABC):
|
|
7
|
+
"""
|
|
8
|
+
Abstract presenter class for presenting output in a given format.
|
|
9
|
+
Subclasses must implement the _format_json_output and _format_plain_output methods.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
debug: bool = False,
|
|
15
|
+
trace: bool = False,
|
|
16
|
+
quiet: bool = False,
|
|
17
|
+
output_file: str = None,
|
|
18
|
+
output_format: str = None,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Initialize the presenter with the given output file and format.
|
|
22
|
+
"""
|
|
23
|
+
self.AVAILABLE_OUTPUT_FORMATS = ['json', 'plain', 'cyclonedx', 'spdxlite', 'csv', 'raw']
|
|
24
|
+
self.base = ScanossBase(debug=debug, trace=trace, quiet=quiet)
|
|
25
|
+
self.output_file = output_file
|
|
26
|
+
self.output_format = output_format
|
|
27
|
+
|
|
28
|
+
def present(self, output_format: str = None, output_file: str = None):
|
|
29
|
+
"""
|
|
30
|
+
Present the formatted output to a file if provided; otherwise, print to stdout.
|
|
31
|
+
"""
|
|
32
|
+
file_path = output_file or self.output_file
|
|
33
|
+
fmt = output_format or self.output_format
|
|
34
|
+
|
|
35
|
+
if fmt and fmt not in self.AVAILABLE_OUTPUT_FORMATS:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"ERROR: Invalid output format '{fmt}'. Valid values are: {', '.join(self.AVAILABLE_OUTPUT_FORMATS)}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if fmt == 'json':
|
|
41
|
+
content = self._format_json_output()
|
|
42
|
+
elif fmt == 'plain':
|
|
43
|
+
content = self._format_plain_output()
|
|
44
|
+
elif fmt == 'cyclonedx':
|
|
45
|
+
content = self._format_cyclonedx_output()
|
|
46
|
+
elif fmt == 'spdxlite':
|
|
47
|
+
content = self._format_spdxlite_output()
|
|
48
|
+
elif fmt == 'csv':
|
|
49
|
+
content = self._format_csv_output()
|
|
50
|
+
elif fmt == 'raw':
|
|
51
|
+
content = self._format_raw_output()
|
|
52
|
+
else:
|
|
53
|
+
content = self._format_plain_output()
|
|
54
|
+
|
|
55
|
+
self._present_output(content, file_path)
|
|
56
|
+
|
|
57
|
+
def _present_output(self, content: str, file_path: str = None):
|
|
58
|
+
"""
|
|
59
|
+
If a file path is provided, write to that file; otherwise, print the content to stdout.
|
|
60
|
+
"""
|
|
61
|
+
self.base.print_to_file_or_stdout(content, file_path)
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def _format_cyclonedx_output(self) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Return a CycloneDX string representation of the data.
|
|
67
|
+
"""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def _format_spdxlite_output(self) -> str:
|
|
72
|
+
"""
|
|
73
|
+
Return a SPDX-Lite string representation of the data.
|
|
74
|
+
"""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def _format_csv_output(self) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Return a CSV string representation of the data.
|
|
81
|
+
"""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def _format_json_output(self) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Return a JSON string representation of the data.
|
|
88
|
+
"""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def _format_plain_output(self) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Return a plain text string representation of the data.
|
|
95
|
+
"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def _format_raw_output(self) -> str:
|
|
100
|
+
"""
|
|
101
|
+
Return a raw string representation of the data.
|
|
102
|
+
"""
|
|
103
|
+
pass
|
scanoss/utils/crc64.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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 struct
|
|
26
|
+
from typing import List
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CRC64:
|
|
30
|
+
"""
|
|
31
|
+
CRC64 ECMA implementation matching Go's hash/crc64 package.
|
|
32
|
+
Uses polynomial: 0xC96C5795D7870F42
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
POLY = 0xC96C5795D7870F42
|
|
36
|
+
_TABLE = None
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
if CRC64._TABLE is None:
|
|
40
|
+
CRC64._TABLE = self._make_table()
|
|
41
|
+
self.crc = 0xFFFFFFFFFFFFFFFF # Initial value
|
|
42
|
+
|
|
43
|
+
def _make_table(self) -> list:
|
|
44
|
+
"""Generate the CRC64 lookup table."""
|
|
45
|
+
table = []
|
|
46
|
+
for i in range(256):
|
|
47
|
+
crc = i
|
|
48
|
+
for _ in range(8):
|
|
49
|
+
if crc & 1:
|
|
50
|
+
crc = (crc >> 1) ^ self.POLY
|
|
51
|
+
else:
|
|
52
|
+
crc >>= 1
|
|
53
|
+
table.append(crc)
|
|
54
|
+
return table
|
|
55
|
+
|
|
56
|
+
def update(self, data: bytes) -> None:
|
|
57
|
+
"""Update the CRC with new data."""
|
|
58
|
+
if isinstance(data, str):
|
|
59
|
+
data = data.encode('utf-8')
|
|
60
|
+
|
|
61
|
+
crc = self.crc
|
|
62
|
+
for b in data:
|
|
63
|
+
crc = (crc >> 8) ^ CRC64._TABLE[(crc ^ b) & 0xFF] # Use class-level table
|
|
64
|
+
self.crc = crc
|
|
65
|
+
|
|
66
|
+
def digest(self) -> int:
|
|
67
|
+
"""Get the current CRC value."""
|
|
68
|
+
return self.crc ^ 0xFFFFFFFFFFFFFFFF # Final XOR value
|
|
69
|
+
|
|
70
|
+
def hexdigest(self):
|
|
71
|
+
"""Get the current CRC value as a hexadecimal string."""
|
|
72
|
+
return format(self.digest(), '016x')
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def checksum(cls, data: bytes) -> int:
|
|
76
|
+
"""Calculate CRC64 checksum for the given data."""
|
|
77
|
+
crc = cls()
|
|
78
|
+
crc.update(data)
|
|
79
|
+
return crc.digest()
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def get_hash_buff(cls, buff: bytes) -> List[bytes]:
|
|
83
|
+
"""
|
|
84
|
+
Get the hash value of the given buffer, and converts it to 8 bytes in big-endian order.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
buff (bytes): The buffer to get the hash value of.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
bytes: The hash value of the given buffer, and converts it to 8 bytes in big-endian order.
|
|
91
|
+
"""
|
|
92
|
+
crc = cls()
|
|
93
|
+
crc.update(buff)
|
|
94
|
+
hash_val = crc.digest()
|
|
95
|
+
|
|
96
|
+
return list(struct.pack('>Q', hash_val))
|