scanoss 1.12.2__py3-none-any.whl → 1.43.1__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/__init__.py +13 -13
- protoc_gen_swagger/options/__init__.py +13 -13
- protoc_gen_swagger/options/annotations_pb2.py +18 -12
- protoc_gen_swagger/options/annotations_pb2.pyi +48 -0
- protoc_gen_swagger/options/annotations_pb2_grpc.py +20 -0
- protoc_gen_swagger/options/openapiv2_pb2.py +110 -99
- protoc_gen_swagger/options/openapiv2_pb2.pyi +1317 -0
- protoc_gen_swagger/options/openapiv2_pb2_grpc.py +20 -0
- scanoss/__init__.py +18 -18
- scanoss/api/__init__.py +17 -17
- scanoss/api/common/__init__.py +17 -17
- scanoss/api/common/v2/__init__.py +17 -17
- scanoss/api/common/v2/scanoss_common_pb2.py +49 -20
- scanoss/api/common/v2/scanoss_common_pb2_grpc.py +25 -0
- scanoss/api/components/__init__.py +17 -17
- scanoss/api/components/v2/__init__.py +17 -17
- scanoss/api/components/v2/scanoss_components_pb2.py +68 -43
- scanoss/api/components/v2/scanoss_components_pb2_grpc.py +83 -22
- scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py +136 -21
- scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py +766 -13
- scanoss/api/dependencies/__init__.py +17 -17
- scanoss/api/dependencies/v2/__init__.py +17 -17
- scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +56 -29
- scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +94 -8
- scanoss/api/geoprovenance/__init__.py +23 -0
- scanoss/api/geoprovenance/v2/__init__.py +23 -0
- scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2.py +92 -0
- scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py +381 -0
- scanoss/api/licenses/__init__.py +23 -0
- scanoss/api/licenses/v2/__init__.py +23 -0
- scanoss/api/licenses/v2/scanoss_licenses_pb2.py +84 -0
- scanoss/api/licenses/v2/scanoss_licenses_pb2_grpc.py +302 -0
- scanoss/api/scanning/__init__.py +17 -17
- scanoss/api/scanning/v2/__init__.py +17 -17
- scanoss/api/scanning/v2/scanoss_scanning_pb2.py +42 -13
- scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py +86 -7
- scanoss/api/semgrep/__init__.py +17 -17
- scanoss/api/semgrep/v2/__init__.py +17 -17
- scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py +50 -23
- scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py +151 -16
- scanoss/api/vulnerabilities/__init__.py +17 -17
- scanoss/api/vulnerabilities/v2/__init__.py +17 -17
- scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py +78 -31
- scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py +282 -18
- scanoss/cli.py +2359 -370
- scanoss/components.py +187 -94
- scanoss/constants.py +22 -0
- scanoss/cryptography.py +308 -0
- scanoss/csvoutput.py +91 -58
- scanoss/cyclonedx.py +221 -63
- scanoss/data/build_date.txt +1 -1
- scanoss/data/osadl-copyleft.json +133 -0
- scanoss/data/scanoss-settings-schema.json +254 -0
- scanoss/delta.py +197 -0
- scanoss/export/__init__.py +23 -0
- scanoss/export/dependency_track.py +227 -0
- scanoss/file_filters.py +582 -0
- scanoss/filecount.py +75 -69
- scanoss/gitlabqualityreport.py +214 -0
- scanoss/header_filter.py +563 -0
- scanoss/inspection/__init__.py +23 -0
- scanoss/inspection/policy_check/__init__.py +0 -0
- scanoss/inspection/policy_check/dependency_track/__init__.py +0 -0
- scanoss/inspection/policy_check/dependency_track/project_violation.py +479 -0
- scanoss/inspection/policy_check/policy_check.py +222 -0
- scanoss/inspection/policy_check/scanoss/__init__.py +0 -0
- scanoss/inspection/policy_check/scanoss/copyleft.py +243 -0
- scanoss/inspection/policy_check/scanoss/undeclared_component.py +309 -0
- scanoss/inspection/summary/__init__.py +0 -0
- scanoss/inspection/summary/component_summary.py +170 -0
- scanoss/inspection/summary/license_summary.py +191 -0
- scanoss/inspection/summary/match_summary.py +341 -0
- scanoss/inspection/utils/file_utils.py +44 -0
- scanoss/inspection/utils/license_utils.py +123 -0
- scanoss/inspection/utils/markdown_utils.py +63 -0
- scanoss/inspection/utils/scan_result_processor.py +417 -0
- scanoss/osadl.py +125 -0
- scanoss/results.py +275 -0
- scanoss/scancodedeps.py +87 -38
- scanoss/scanner.py +431 -539
- scanoss/scanners/__init__.py +23 -0
- scanoss/scanners/container_scanner.py +476 -0
- scanoss/scanners/folder_hasher.py +358 -0
- scanoss/scanners/scanner_config.py +73 -0
- scanoss/scanners/scanner_hfh.py +252 -0
- scanoss/scanoss_settings.py +337 -0
- scanoss/scanossapi.py +140 -101
- scanoss/scanossbase.py +59 -22
- scanoss/scanossgrpc.py +799 -251
- scanoss/scanpostprocessor.py +294 -0
- scanoss/scantype.py +22 -21
- scanoss/services/dependency_track_service.py +132 -0
- scanoss/spdxlite.py +532 -174
- scanoss/threadeddependencies.py +148 -47
- scanoss/threadedscanning.py +53 -37
- scanoss/utils/__init__.py +23 -0
- scanoss/utils/abstract_presenter.py +103 -0
- scanoss/utils/crc64.py +96 -0
- scanoss/utils/file.py +84 -0
- scanoss/utils/scanoss_scan_results_utils.py +41 -0
- scanoss/utils/simhash.py +198 -0
- scanoss/winnowing.py +241 -63
- {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/METADATA +18 -9
- scanoss-1.43.1.dist-info/RECORD +110 -0
- {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/WHEEL +1 -1
- scanoss-1.12.2.dist-info/RECORD +0 -58
- {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/entry_points.txt +0 -0
- {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info/licenses}/LICENSE +0 -0
- {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/top_level.txt +0 -0
|
@@ -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,476 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024, 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 json
|
|
26
|
+
import os
|
|
27
|
+
import subprocess
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Dict, List, Optional, TypedDict
|
|
30
|
+
|
|
31
|
+
from scanoss.constants import DEFAULT_RETRY, DEFAULT_TIMEOUT
|
|
32
|
+
from scanoss.csvoutput import CsvOutput
|
|
33
|
+
from scanoss.cyclonedx import CycloneDx
|
|
34
|
+
from scanoss.scanossbase import ScanossBase
|
|
35
|
+
from scanoss.scanossgrpc import ScanossGrpc
|
|
36
|
+
from scanoss.spdxlite import SpdxLite
|
|
37
|
+
from scanoss.utils.abstract_presenter import AbstractPresenter
|
|
38
|
+
|
|
39
|
+
DEFAULT_SYFT_TIMEOUT = 600
|
|
40
|
+
DEFAULT_SYFT_COMMAND = 'syft'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ContainerScannerConfig:
|
|
45
|
+
debug: bool = False
|
|
46
|
+
trace: bool = False
|
|
47
|
+
quiet: bool = False
|
|
48
|
+
retry: int = DEFAULT_RETRY
|
|
49
|
+
timeout: int = DEFAULT_TIMEOUT
|
|
50
|
+
output: Optional[str] = None
|
|
51
|
+
format: Optional[str] = None
|
|
52
|
+
apiurl: Optional[str] = None
|
|
53
|
+
ignore_cert_errors: bool = False
|
|
54
|
+
key: Optional[str] = None
|
|
55
|
+
proxy: Optional[str] = None
|
|
56
|
+
pac: Optional[str] = None
|
|
57
|
+
grpc_proxy: Optional[str] = None
|
|
58
|
+
ca_cert: Optional[str] = None
|
|
59
|
+
syft_command: str = DEFAULT_SYFT_COMMAND
|
|
60
|
+
syft_timeout: int = DEFAULT_SYFT_TIMEOUT
|
|
61
|
+
only_interim_results: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_container_scanner_config_from_args(args) -> ContainerScannerConfig:
|
|
65
|
+
return ContainerScannerConfig(
|
|
66
|
+
debug=args.debug if 'debug' in args else False,
|
|
67
|
+
trace=args.trace if 'trace' in args else False,
|
|
68
|
+
quiet=args.quiet if 'quiet' in args else False,
|
|
69
|
+
retry=args.retry if 'retry' in args else DEFAULT_RETRY,
|
|
70
|
+
timeout=args.timeout if 'timeout' in args else DEFAULT_TIMEOUT,
|
|
71
|
+
output=args.output if 'output' in args else None,
|
|
72
|
+
format=args.format if 'format' in args else None,
|
|
73
|
+
apiurl=args.api2url if 'api2url' in args else None,
|
|
74
|
+
proxy=args.proxy if 'proxy' in args else None,
|
|
75
|
+
pac=args.pac if 'pac' in args else None,
|
|
76
|
+
grpc_proxy=args.grpc_proxy if 'grpc_proxy' in args else None,
|
|
77
|
+
ca_cert=args.ca_cert if 'ca_cert' in args else None,
|
|
78
|
+
ignore_cert_errors=args.ignore_cert_errors if 'ignore_cert_errors' in args else False,
|
|
79
|
+
key=args.key if 'key' in args else None,
|
|
80
|
+
syft_command=args.syft_command if 'syft_command' in args else DEFAULT_SYFT_COMMAND,
|
|
81
|
+
syft_timeout=args.syft_timeout if 'syft_timeout' in args else DEFAULT_SYFT_TIMEOUT,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class LicenseItem(TypedDict):
|
|
86
|
+
value: str
|
|
87
|
+
spdxExpression: str
|
|
88
|
+
type: str
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_dict(cls, data: dict):
|
|
92
|
+
return cls(**data)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SyftArtifactItem(TypedDict):
|
|
96
|
+
name: str
|
|
97
|
+
version: str
|
|
98
|
+
type: str
|
|
99
|
+
purl: str
|
|
100
|
+
licenses: List[LicenseItem]
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_dict(cls, data: dict):
|
|
104
|
+
return cls(
|
|
105
|
+
name=data['name'],
|
|
106
|
+
version=data['version'],
|
|
107
|
+
type=data['type'],
|
|
108
|
+
purl=data['purl'],
|
|
109
|
+
licenses=[LicenseItem.from_dict(lic) for lic in data['licenses']],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SyftScanResult(TypedDict):
|
|
114
|
+
artifacts: List[SyftArtifactItem]
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_dict(cls, data: dict):
|
|
118
|
+
return cls(artifacts=[SyftArtifactItem.from_dict(a) for a in data['artifacts']])
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class PurlItem(TypedDict):
|
|
122
|
+
purl: str
|
|
123
|
+
requirement: Optional[str]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ContainerScanResultFileItem(TypedDict):
|
|
127
|
+
file: str
|
|
128
|
+
purls: List[PurlItem]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ContainerScanResult(TypedDict):
|
|
132
|
+
files: List[ContainerScanResultFileItem]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class DependencyLicenseItem(TypedDict):
|
|
136
|
+
value: str
|
|
137
|
+
spdxExpression: str
|
|
138
|
+
type: str
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class DependencyItem(TypedDict):
|
|
142
|
+
purl: str
|
|
143
|
+
licenses: List[DependencyLicenseItem]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class DecoratedContainerScanResultFileItem(TypedDict):
|
|
147
|
+
file: str
|
|
148
|
+
id: str
|
|
149
|
+
status: str
|
|
150
|
+
dependencies: List[DependencyItem]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class DecoratedContainerScanResult(TypedDict):
|
|
154
|
+
files: List[ContainerScanResultFileItem]
|
|
155
|
+
status: Dict[str, str]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SyftScanError(Exception):
|
|
159
|
+
"""Base exception for Syft scan errors"""
|
|
160
|
+
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class SyftExecutionError(SyftScanError):
|
|
165
|
+
"""Raised when Syft returns a non-zero exit code"""
|
|
166
|
+
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class SyftJsonError(SyftScanError):
|
|
171
|
+
"""Raised when Syft output cannot be parsed as JSON"""
|
|
172
|
+
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class SyftTimeoutError(SyftScanError):
|
|
177
|
+
"""Raised when a Syft scan times out"""
|
|
178
|
+
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class SCANOSSDependencyScanError(Exception):
|
|
183
|
+
"""Base exception for SCANOSS dependency scan errors"""
|
|
184
|
+
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class DecorateScanResultsError(SCANOSSDependencyScanError):
|
|
189
|
+
"""Raised when there is an issue decorating scan results with dependencies"""
|
|
190
|
+
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ContainerScanner:
|
|
195
|
+
"""SCANOSS container scanning class.
|
|
196
|
+
|
|
197
|
+
This class provides functionality to scan containers using Syft and process
|
|
198
|
+
the results into SCANOSS dependency format.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
config: ContainerScannerConfig,
|
|
204
|
+
what_to_scan: str,
|
|
205
|
+
):
|
|
206
|
+
"""Initialize ContainerScanner class.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
config: ContainerScannerConfig object containing configuration settings.
|
|
210
|
+
"""
|
|
211
|
+
self.base = ScanossBase(
|
|
212
|
+
debug=config.debug,
|
|
213
|
+
trace=config.trace,
|
|
214
|
+
quiet=config.quiet,
|
|
215
|
+
)
|
|
216
|
+
self.presenter = ContainerScannerPresenter(
|
|
217
|
+
self,
|
|
218
|
+
debug=config.debug,
|
|
219
|
+
trace=config.trace,
|
|
220
|
+
quiet=config.quiet,
|
|
221
|
+
output_file=config.output,
|
|
222
|
+
output_format=config.format,
|
|
223
|
+
)
|
|
224
|
+
self.grpc_api = ScanossGrpc(
|
|
225
|
+
debug=config.debug,
|
|
226
|
+
quiet=config.quiet,
|
|
227
|
+
trace=config.trace,
|
|
228
|
+
url=config.apiurl,
|
|
229
|
+
api_key=config.key,
|
|
230
|
+
ca_cert=config.ca_cert,
|
|
231
|
+
proxy=config.proxy,
|
|
232
|
+
pac=config.pac,
|
|
233
|
+
grpc_proxy=config.grpc_proxy,
|
|
234
|
+
)
|
|
235
|
+
self.what_to_scan: str = what_to_scan
|
|
236
|
+
self.syft_command: str = config.syft_command
|
|
237
|
+
self.syft_timeout: int = config.syft_timeout
|
|
238
|
+
self.only_interim_results: bool = config.only_interim_results
|
|
239
|
+
self.syft_output: Optional[SyftScanResult] = None
|
|
240
|
+
self.normalized_syft_output: Optional[ContainerScanResult] = None
|
|
241
|
+
self.decorated_scan_results: Optional[DecoratedContainerScanResult] = None
|
|
242
|
+
|
|
243
|
+
def decorate_scan_results_with_dependencies(self) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Decorate the scan results with dependencies.
|
|
246
|
+
"""
|
|
247
|
+
try:
|
|
248
|
+
decorated_scan_results = self.grpc_api.get_dependencies(self.normalized_syft_output)
|
|
249
|
+
self.decorated_scan_results = decorated_scan_results
|
|
250
|
+
return decorated_scan_results
|
|
251
|
+
except Exception as e:
|
|
252
|
+
error_msg = f'Issue decorating scan results with dependencies: {e}'
|
|
253
|
+
self.base.print_stderr(f'ERROR: {error_msg}')
|
|
254
|
+
raise DecorateScanResultsError(error_msg) from e
|
|
255
|
+
|
|
256
|
+
def scan(
|
|
257
|
+
self,
|
|
258
|
+
) -> ContainerScanResult:
|
|
259
|
+
"""Run a syft scan of the specified target.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
ContainerScanResult: The container scan results.
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
SyftScanError: For other scan-related errors
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
self.syft_output = self._execute_syft_scan()
|
|
269
|
+
self.normalized_syft_output = self._normalize_syft_output()
|
|
270
|
+
return self.normalized_syft_output
|
|
271
|
+
except Exception as e:
|
|
272
|
+
if isinstance(e, SyftScanError):
|
|
273
|
+
raise
|
|
274
|
+
error_msg = f'Issue running syft scan on {self.what_to_scan}: {e}'
|
|
275
|
+
self.base.print_stderr(f'ERROR: {error_msg}')
|
|
276
|
+
raise SyftScanError(error_msg) from e
|
|
277
|
+
|
|
278
|
+
def _execute_syft_scan(self) -> SyftScanResult:
|
|
279
|
+
"""
|
|
280
|
+
Execute a Syft scan of the specified target.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
SyftScanResult: The result of the Syft scan.
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
SyftScanError: If the Syft scan fails.
|
|
287
|
+
SyftJsonError: If the Syft scan output cannot be parsed as JSON.
|
|
288
|
+
SyftTimeoutError: If the Syft scan times out.
|
|
289
|
+
SyftExecutionError: If the Syft scan execution fails.
|
|
290
|
+
"""
|
|
291
|
+
try:
|
|
292
|
+
self.base.print_trace(f'About to execute {self.syft_command} scan {self.what_to_scan} -q -o json')
|
|
293
|
+
self.base.print_msg('Scanning container...')
|
|
294
|
+
result = subprocess.run(
|
|
295
|
+
[self.syft_command, 'scan', self.what_to_scan, '-q', '-o', 'json'],
|
|
296
|
+
cwd=os.getcwd(),
|
|
297
|
+
stdout=subprocess.PIPE,
|
|
298
|
+
stderr=subprocess.STDOUT,
|
|
299
|
+
text=True,
|
|
300
|
+
timeout=self.syft_timeout,
|
|
301
|
+
check=False,
|
|
302
|
+
)
|
|
303
|
+
self.base.print_trace(f'Subprocess return: {result}')
|
|
304
|
+
|
|
305
|
+
if result.returncode:
|
|
306
|
+
error_msg = (
|
|
307
|
+
f'Syft scan of {self.what_to_scan} failed with exit code {result.returncode}:\n{result.stdout}'
|
|
308
|
+
)
|
|
309
|
+
self.base.print_stderr(f'ERROR: {error_msg}')
|
|
310
|
+
raise SyftExecutionError(error_msg)
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
json_data = json.loads(result.stdout)
|
|
314
|
+
return SyftScanResult.from_dict(json_data)
|
|
315
|
+
except json.JSONDecodeError as e:
|
|
316
|
+
error_msg = f'Failed to parse JSON output from syft: {e}\n{result.stdout}'
|
|
317
|
+
self.base.print_stderr(f'ERROR: {error_msg}')
|
|
318
|
+
raise SyftJsonError(error_msg) from e
|
|
319
|
+
|
|
320
|
+
except subprocess.TimeoutExpired as e:
|
|
321
|
+
error_msg = f'Timed out attempting to run syft scan on {self.what_to_scan}: {e}'
|
|
322
|
+
self.base.print_stderr(f'ERROR: {error_msg}')
|
|
323
|
+
raise SyftTimeoutError(error_msg) from e
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
if isinstance(e, SyftScanError):
|
|
327
|
+
raise
|
|
328
|
+
error_msg = f'Issue running syft scan on {self.what_to_scan}: {e}'
|
|
329
|
+
self.base.print_stderr(f'ERROR: {error_msg}')
|
|
330
|
+
raise SyftScanError(error_msg) from e
|
|
331
|
+
|
|
332
|
+
def _get_dependencies(self) -> None:
|
|
333
|
+
"""
|
|
334
|
+
Run a dependency scan of the specified target.
|
|
335
|
+
"""
|
|
336
|
+
try:
|
|
337
|
+
if not self.normalized_syft_output:
|
|
338
|
+
error_msg = 'Syft scan output is not available'
|
|
339
|
+
self.base.print_stderr(error_msg)
|
|
340
|
+
raise ValueError(error_msg)
|
|
341
|
+
if not self.grpc_api.get_dependencies(self.normalized_syft_output):
|
|
342
|
+
error_msg = 'Failed to get dependencies'
|
|
343
|
+
self.base.print_stderr(error_msg)
|
|
344
|
+
raise SCANOSSDependencyScanError(error_msg)
|
|
345
|
+
except Exception as e:
|
|
346
|
+
error_msg = f'Failed to run dependency scan: {e}'
|
|
347
|
+
self.base.print_stderr(error_msg)
|
|
348
|
+
raise SCANOSSDependencyScanError(error_msg)
|
|
349
|
+
|
|
350
|
+
def _normalize_syft_output(self) -> ContainerScanResult:
|
|
351
|
+
"""
|
|
352
|
+
Normalize the Syft output data into the same format we use in dependency scanning
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
ContainerScanResult: The normalized output
|
|
356
|
+
"""
|
|
357
|
+
normalized_output = ContainerScanResult()
|
|
358
|
+
|
|
359
|
+
# This is a workaround because we don't have file paths as in dependency scanning, we use the container name
|
|
360
|
+
file_name = self.what_to_scan
|
|
361
|
+
artifacts = self.syft_output['artifacts']
|
|
362
|
+
|
|
363
|
+
unique_purls = set()
|
|
364
|
+
unique_purl_items = []
|
|
365
|
+
|
|
366
|
+
for artifact in artifacts:
|
|
367
|
+
purl = artifact['purl']
|
|
368
|
+
if purl not in unique_purls:
|
|
369
|
+
unique_purls.add(purl)
|
|
370
|
+
unique_purl_items.append(PurlItem(purl=purl))
|
|
371
|
+
|
|
372
|
+
normalized_output['files'] = [
|
|
373
|
+
{
|
|
374
|
+
'file': file_name,
|
|
375
|
+
'purls': unique_purl_items,
|
|
376
|
+
}
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
return normalized_output
|
|
380
|
+
|
|
381
|
+
def present(self, output_format: str = None, output_file: str = None):
|
|
382
|
+
"""Present the results in the selected format"""
|
|
383
|
+
self.presenter.present(output_format=output_format, output_file=output_file)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class ContainerScannerPresenter(AbstractPresenter):
|
|
387
|
+
"""
|
|
388
|
+
ContainerScannerPresenter presenter class
|
|
389
|
+
Handles the presentation of the container scan results
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
def __init__(self, scanner: ContainerScanner, **kwargs):
|
|
393
|
+
super().__init__(**kwargs)
|
|
394
|
+
self.scanner = scanner
|
|
395
|
+
self.AVAILABLE_OUTPUT_FORMATS = ['plain', 'cyclonedx', 'spdxlite', 'csv', 'raw']
|
|
396
|
+
|
|
397
|
+
def _convert_raw_to_scan_output(self) -> dict:
|
|
398
|
+
"""
|
|
399
|
+
Convert the raw output from dependency scanning API to our scan output format
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
dict: The converted output
|
|
403
|
+
"""
|
|
404
|
+
formatted_output = {}
|
|
405
|
+
if (
|
|
406
|
+
self.scanner.decorated_scan_results
|
|
407
|
+
and 'files' in self.scanner.decorated_scan_results
|
|
408
|
+
and self.scanner.decorated_scan_results['files']
|
|
409
|
+
and isinstance(self.scanner.decorated_scan_results['files'], list)
|
|
410
|
+
):
|
|
411
|
+
file_item = self.scanner.decorated_scan_results['files'][0]
|
|
412
|
+
if file_item and isinstance(file_item, dict) and 'file' in file_item:
|
|
413
|
+
formatted_output[file_item['file']] = [file_item]
|
|
414
|
+
|
|
415
|
+
return formatted_output
|
|
416
|
+
|
|
417
|
+
def _format_plain_output(self) -> str:
|
|
418
|
+
"""
|
|
419
|
+
Format the scan output data into a plain text string
|
|
420
|
+
"""
|
|
421
|
+
return json.dumps(self._convert_raw_to_scan_output(), indent=2)
|
|
422
|
+
|
|
423
|
+
def _format_raw_output(self) -> str:
|
|
424
|
+
"""
|
|
425
|
+
Format the scan output data into the raw output from dependency scanning API
|
|
426
|
+
"""
|
|
427
|
+
if self.scanner.only_interim_results:
|
|
428
|
+
return json.dumps(self.scanner.normalized_syft_output, indent=2)
|
|
429
|
+
return json.dumps(self.scanner.decorated_scan_results, indent=2)
|
|
430
|
+
|
|
431
|
+
def _format_cyclonedx_output(self) -> str:
|
|
432
|
+
"""
|
|
433
|
+
Format the scan output data into a CycloneDX object
|
|
434
|
+
"""
|
|
435
|
+
cdx = CycloneDx(self.base.debug, self.output_file)
|
|
436
|
+
scan_results = {}
|
|
437
|
+
for f in self.scanner.decorated_scan_results['files']:
|
|
438
|
+
scan_results[f['file']] = [f]
|
|
439
|
+
success, cdx_output = cdx.produce_from_json(scan_results)
|
|
440
|
+
if not success:
|
|
441
|
+
error_msg = 'Failed to produce CycloneDX output'
|
|
442
|
+
self.base.print_stderr(error_msg)
|
|
443
|
+
return None
|
|
444
|
+
return json.dumps(cdx_output, indent=2)
|
|
445
|
+
|
|
446
|
+
def _format_spdxlite_output(self) -> str:
|
|
447
|
+
"""
|
|
448
|
+
Format the scan output data into a SPDXLite object
|
|
449
|
+
"""
|
|
450
|
+
spdxlite = SpdxLite(self.base.debug, self.output_file)
|
|
451
|
+
scan_results = {}
|
|
452
|
+
for f in self.scanner.decorated_scan_results['files']:
|
|
453
|
+
scan_results[f['file']] = [f]
|
|
454
|
+
if not spdxlite.produce_from_json(scan_results, self.output_file):
|
|
455
|
+
error_msg = 'Failed to produce SPDXLite output'
|
|
456
|
+
self.base.print_stderr(error_msg)
|
|
457
|
+
raise ValueError(error_msg)
|
|
458
|
+
|
|
459
|
+
def _format_csv_output(self) -> str:
|
|
460
|
+
"""
|
|
461
|
+
Format the scan output data into a CSV object
|
|
462
|
+
"""
|
|
463
|
+
csv = CsvOutput(self.base.debug, self.output_file)
|
|
464
|
+
scan_results = {}
|
|
465
|
+
for f in self.scanner.decorated_scan_results['files']:
|
|
466
|
+
scan_results[f['file']] = [f]
|
|
467
|
+
if not csv.produce_from_json(scan_results, self.output_file):
|
|
468
|
+
error_msg = 'Failed to produce CSV output'
|
|
469
|
+
self.base.print_stderr(error_msg)
|
|
470
|
+
raise ValueError(error_msg)
|
|
471
|
+
|
|
472
|
+
def _format_json_output(self) -> str:
|
|
473
|
+
"""
|
|
474
|
+
Format the scan output data into a JSON object
|
|
475
|
+
"""
|
|
476
|
+
pass
|