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.
Files changed (109) hide show
  1. protoc_gen_swagger/__init__.py +13 -13
  2. protoc_gen_swagger/options/__init__.py +13 -13
  3. protoc_gen_swagger/options/annotations_pb2.py +18 -12
  4. protoc_gen_swagger/options/annotations_pb2.pyi +48 -0
  5. protoc_gen_swagger/options/annotations_pb2_grpc.py +20 -0
  6. protoc_gen_swagger/options/openapiv2_pb2.py +110 -99
  7. protoc_gen_swagger/options/openapiv2_pb2.pyi +1317 -0
  8. protoc_gen_swagger/options/openapiv2_pb2_grpc.py +20 -0
  9. scanoss/__init__.py +18 -18
  10. scanoss/api/__init__.py +17 -17
  11. scanoss/api/common/__init__.py +17 -17
  12. scanoss/api/common/v2/__init__.py +17 -17
  13. scanoss/api/common/v2/scanoss_common_pb2.py +49 -20
  14. scanoss/api/common/v2/scanoss_common_pb2_grpc.py +25 -0
  15. scanoss/api/components/__init__.py +17 -17
  16. scanoss/api/components/v2/__init__.py +17 -17
  17. scanoss/api/components/v2/scanoss_components_pb2.py +68 -43
  18. scanoss/api/components/v2/scanoss_components_pb2_grpc.py +83 -22
  19. scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py +136 -21
  20. scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py +766 -13
  21. scanoss/api/dependencies/__init__.py +17 -17
  22. scanoss/api/dependencies/v2/__init__.py +17 -17
  23. scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +56 -29
  24. scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +94 -8
  25. scanoss/api/geoprovenance/__init__.py +23 -0
  26. scanoss/api/geoprovenance/v2/__init__.py +23 -0
  27. scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2.py +92 -0
  28. scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py +381 -0
  29. scanoss/api/licenses/__init__.py +23 -0
  30. scanoss/api/licenses/v2/__init__.py +23 -0
  31. scanoss/api/licenses/v2/scanoss_licenses_pb2.py +84 -0
  32. scanoss/api/licenses/v2/scanoss_licenses_pb2_grpc.py +302 -0
  33. scanoss/api/scanning/__init__.py +17 -17
  34. scanoss/api/scanning/v2/__init__.py +17 -17
  35. scanoss/api/scanning/v2/scanoss_scanning_pb2.py +42 -13
  36. scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py +86 -7
  37. scanoss/api/semgrep/__init__.py +17 -17
  38. scanoss/api/semgrep/v2/__init__.py +17 -17
  39. scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py +50 -23
  40. scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py +151 -16
  41. scanoss/api/vulnerabilities/__init__.py +17 -17
  42. scanoss/api/vulnerabilities/v2/__init__.py +17 -17
  43. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py +78 -31
  44. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py +282 -18
  45. scanoss/cli.py +2359 -370
  46. scanoss/components.py +187 -94
  47. scanoss/constants.py +22 -0
  48. scanoss/cryptography.py +308 -0
  49. scanoss/csvoutput.py +91 -58
  50. scanoss/cyclonedx.py +221 -63
  51. scanoss/data/build_date.txt +1 -1
  52. scanoss/data/osadl-copyleft.json +133 -0
  53. scanoss/data/scanoss-settings-schema.json +254 -0
  54. scanoss/delta.py +197 -0
  55. scanoss/export/__init__.py +23 -0
  56. scanoss/export/dependency_track.py +227 -0
  57. scanoss/file_filters.py +582 -0
  58. scanoss/filecount.py +75 -69
  59. scanoss/gitlabqualityreport.py +214 -0
  60. scanoss/header_filter.py +563 -0
  61. scanoss/inspection/__init__.py +23 -0
  62. scanoss/inspection/policy_check/__init__.py +0 -0
  63. scanoss/inspection/policy_check/dependency_track/__init__.py +0 -0
  64. scanoss/inspection/policy_check/dependency_track/project_violation.py +479 -0
  65. scanoss/inspection/policy_check/policy_check.py +222 -0
  66. scanoss/inspection/policy_check/scanoss/__init__.py +0 -0
  67. scanoss/inspection/policy_check/scanoss/copyleft.py +243 -0
  68. scanoss/inspection/policy_check/scanoss/undeclared_component.py +309 -0
  69. scanoss/inspection/summary/__init__.py +0 -0
  70. scanoss/inspection/summary/component_summary.py +170 -0
  71. scanoss/inspection/summary/license_summary.py +191 -0
  72. scanoss/inspection/summary/match_summary.py +341 -0
  73. scanoss/inspection/utils/file_utils.py +44 -0
  74. scanoss/inspection/utils/license_utils.py +123 -0
  75. scanoss/inspection/utils/markdown_utils.py +63 -0
  76. scanoss/inspection/utils/scan_result_processor.py +417 -0
  77. scanoss/osadl.py +125 -0
  78. scanoss/results.py +275 -0
  79. scanoss/scancodedeps.py +87 -38
  80. scanoss/scanner.py +431 -539
  81. scanoss/scanners/__init__.py +23 -0
  82. scanoss/scanners/container_scanner.py +476 -0
  83. scanoss/scanners/folder_hasher.py +358 -0
  84. scanoss/scanners/scanner_config.py +73 -0
  85. scanoss/scanners/scanner_hfh.py +252 -0
  86. scanoss/scanoss_settings.py +337 -0
  87. scanoss/scanossapi.py +140 -101
  88. scanoss/scanossbase.py +59 -22
  89. scanoss/scanossgrpc.py +799 -251
  90. scanoss/scanpostprocessor.py +294 -0
  91. scanoss/scantype.py +22 -21
  92. scanoss/services/dependency_track_service.py +132 -0
  93. scanoss/spdxlite.py +532 -174
  94. scanoss/threadeddependencies.py +148 -47
  95. scanoss/threadedscanning.py +53 -37
  96. scanoss/utils/__init__.py +23 -0
  97. scanoss/utils/abstract_presenter.py +103 -0
  98. scanoss/utils/crc64.py +96 -0
  99. scanoss/utils/file.py +84 -0
  100. scanoss/utils/scanoss_scan_results_utils.py +41 -0
  101. scanoss/utils/simhash.py +198 -0
  102. scanoss/winnowing.py +241 -63
  103. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/METADATA +18 -9
  104. scanoss-1.43.1.dist-info/RECORD +110 -0
  105. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/WHEEL +1 -1
  106. scanoss-1.12.2.dist-info/RECORD +0 -58
  107. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/entry_points.txt +0 -0
  108. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info/licenses}/LICENSE +0 -0
  109. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,308 @@
1
+ import json
2
+ from dataclasses import dataclass
3
+ from typing import Dict, List, Optional
4
+
5
+ from scanoss.cyclonedx import CycloneDx
6
+ from scanoss.scanossbase import ScanossBase
7
+ from scanoss.scanossgrpc import ScanossGrpc
8
+ from scanoss.utils.abstract_presenter import AbstractPresenter
9
+ from scanoss.utils.file import validate_json_file
10
+
11
+
12
+ class ScanossCryptographyError(Exception):
13
+ pass
14
+
15
+
16
+ MIN_SPLIT_PARTS = 2
17
+
18
+
19
+ @dataclass
20
+ class CryptographyConfig:
21
+ purl: List[str]
22
+ debug: bool = False
23
+ header: Optional[str] = None
24
+ input_file: Optional[str] = None
25
+ output_file: Optional[str] = None
26
+ quiet: bool = False
27
+ trace: bool = False
28
+ use_grpc: bool = False
29
+ with_range: bool = False
30
+
31
+ def _process_input_file(self) -> dict:
32
+ """
33
+ Process and validate the input file, returning the validated purl_request.
34
+
35
+ Returns:
36
+ dict: The validated purl_request dictionary
37
+
38
+ Raises:
39
+ ScanossCryptographyError: If the input file is invalid
40
+ """
41
+ result = validate_json_file(self.input_file)
42
+ if not result.is_valid:
43
+ raise ScanossCryptographyError(
44
+ f'There was a problem with the purl input file. {result.error}'
45
+ )
46
+
47
+ cdx = CycloneDx(debug=self.debug)
48
+ if cdx.is_cyclonedx_json(json.dumps(result.data)):
49
+ purl_request = cdx.get_purls_request_from_cdx(result.data)
50
+ else:
51
+ purl_request = result.data
52
+
53
+ if (
54
+ not isinstance(purl_request, dict)
55
+ or 'purls' not in purl_request
56
+ or not isinstance(purl_request['purls'], list)
57
+ or not all(isinstance(p, dict) and 'purl' in p for p in purl_request['purls'])
58
+ ):
59
+ raise ScanossCryptographyError('The supplied input file is not in the correct PurlRequest format.')
60
+
61
+ return purl_request
62
+
63
+ def __post_init__(self):
64
+ """
65
+ Validate that the configuration is valid.
66
+ """
67
+ if self.purl:
68
+ if self.with_range:
69
+ for purl in self.purl:
70
+ parts = purl.split('@')
71
+ if not (len(parts) >= MIN_SPLIT_PARTS and parts[1]):
72
+ raise ScanossCryptographyError(
73
+ f'Invalid PURL format: "{purl}".It must include a version (e.g., pkg:type/name@version)'
74
+ )
75
+ if self.input_file:
76
+ purl_request = self._process_input_file()
77
+ purls = purl_request['purls']
78
+ purls_with_requirement = []
79
+ if self.with_range and any('requirement' not in p for p in purls):
80
+ raise ScanossCryptographyError(
81
+ f'One or more PURLs in "{self.input_file}" are missing the "requirement" field.'
82
+ )
83
+
84
+ for purl in purls:
85
+ if 'requirement' in purl:
86
+ purls_with_requirement.append(f'{purl["purl"]}@{purl["requirement"]}')
87
+ else:
88
+ purls_with_requirement.append(purl['purl'])
89
+ self.purl = purls_with_requirement
90
+
91
+
92
+ def create_cryptography_config_from_args(args) -> CryptographyConfig:
93
+ return CryptographyConfig(
94
+ debug=getattr(args, 'debug', False),
95
+ header=getattr(args, 'header', None),
96
+ input_file=getattr(args, 'input', None),
97
+ output_file=getattr(args, 'output', None),
98
+ purl=getattr(args, 'purl', []),
99
+ quiet=getattr(args, 'quiet', False),
100
+ trace=getattr(args, 'trace', False),
101
+ use_grpc=getattr(args, 'grpc', False),
102
+ with_range=getattr(args, 'with_range', False),
103
+ )
104
+
105
+
106
+ class Cryptography:
107
+ """
108
+ Cryptography Class
109
+
110
+ This class is used to decorate purls with cryptography information.
111
+ """
112
+
113
+ def __init__(
114
+ self,
115
+ config: CryptographyConfig,
116
+ client: ScanossGrpc,
117
+ ):
118
+ """
119
+ Initialize the Cryptography.
120
+
121
+ Args:
122
+ config (CryptographyConfig): Configuration parameters for the cryptography.
123
+ client (ScanossGrpc): gRPC client for communicating with the scanning service.
124
+ """
125
+ self.base = ScanossBase(
126
+ debug=config.debug,
127
+ trace=config.trace,
128
+ quiet=config.quiet,
129
+ )
130
+ self.presenter = CryptographyPresenter(
131
+ self,
132
+ debug=config.debug,
133
+ trace=config.trace,
134
+ quiet=config.quiet,
135
+ )
136
+
137
+ self.client = client
138
+ self.config = config
139
+ self.components_request = self._build_components_request()
140
+ self.results = None
141
+
142
+ def get_algorithms(self) -> Optional[Dict]:
143
+ """
144
+ Get the cryptographic algorithms for the provided purl or input file.
145
+
146
+ Returns:
147
+ Optional[Dict]: The folder hash response from the gRPC client, or None if an error occurs.
148
+ """
149
+
150
+ if not self.components_request or not self.components_request.get('components'):
151
+ raise ScanossCryptographyError('No PURLs supplied. Provide --purl or --input.')
152
+ components_str = ', '.join(p['purl'] for p in self.components_request['components'])
153
+ self.base.print_stderr(f'Getting cryptographic algorithms for {components_str}')
154
+ if self.config.with_range:
155
+ response = self.client.get_crypto_algorithms_in_range_for_purl(
156
+ self.components_request, self.config.use_grpc
157
+ )
158
+ else:
159
+ response = self.client.get_crypto_algorithms_for_purl(self.components_request, self.config.use_grpc)
160
+ if response:
161
+ self.results = response
162
+
163
+ return self.results
164
+
165
+ def get_encryption_hints(self) -> Optional[Dict]:
166
+ """
167
+ Get the encryption hints for the provided purl or input file.
168
+
169
+ Returns:
170
+ Optional[Dict]: The encryption hints response from the gRPC client, or None if an error occurs.
171
+ """
172
+
173
+ if not self.components_request or not self.components_request.get('components'):
174
+ raise ScanossCryptographyError('No PURLs supplied. Provide --purl or --input.')
175
+ self.base.print_stderr(
176
+ f'Getting encryption hints '
177
+ f'{"in range" if self.config.with_range else ""} '
178
+ f'for {", ".join([p["purl"] for p in self.components_request["components"]])}'
179
+ )
180
+ if self.config.with_range:
181
+ response = self.client.get_encryption_hints_in_range_for_purl(self.components_request, self.config.use_grpc)
182
+ else:
183
+ response = self.client.get_encryption_hints_for_purl(self.components_request, self.config.use_grpc)
184
+ if response:
185
+ self.results = response
186
+
187
+ return self.results
188
+
189
+ def get_versions_in_range(self) -> Optional[Dict]:
190
+ """
191
+ Given a list of PURLS and version ranges, get a list of versions that do/do not contain cryptographic algorithms
192
+
193
+ Returns:
194
+ Optional[Dict]: The versions in range response from the gRPC client, or None if an error occurs.
195
+ """
196
+
197
+ if not self.components_request or not self.components_request.get('components'):
198
+ raise ScanossCryptographyError('No PURLs supplied. Provide --purl or --input.')
199
+
200
+ components_str = ', '.join(p['purl'] for p in self.components_request['components'])
201
+ self.base.print_stderr(f'Getting versions in range for {components_str}')
202
+
203
+ response = self.client.get_versions_in_range_for_purl(self.components_request, self.config.use_grpc)
204
+ if response:
205
+ self.results = response
206
+
207
+ return self.results
208
+
209
+ def _build_components_request(
210
+ self,
211
+ ) -> Optional[dict]:
212
+ """
213
+ Load the specified purls from a JSON file or a list of PURLs and return a dictionary
214
+
215
+ Returns:
216
+ Optional[dict]: The dictionary containing the PURLs
217
+ """
218
+ return {
219
+ 'components': [
220
+ {
221
+ 'purl': self._remove_version_from_purl(purl),
222
+ 'requirement': self._extract_version_from_purl(purl),
223
+ }
224
+ for purl in self.config.purl
225
+ ]
226
+ }
227
+
228
+ def _remove_version_from_purl(self, purl: str) -> str:
229
+ """
230
+ Remove version from purl
231
+
232
+ Args:
233
+ purl (str): The purl string to remove the version from
234
+
235
+ Returns:
236
+ str: The purl string without the version
237
+ """
238
+ if '@' not in purl:
239
+ return purl
240
+ return purl.split('@')[0]
241
+
242
+ def _extract_version_from_purl(self, purl: str) -> str:
243
+ """
244
+ Extract version from purl
245
+
246
+ Args:
247
+ purl (str): The purl string to extract the version from
248
+
249
+ Returns:
250
+ str: The extracted version
251
+
252
+ Raises:
253
+ ScanossCryptographyError: If the purl is not in the correct format
254
+ """
255
+ try:
256
+ return purl.split('@')[-1]
257
+ except IndexError:
258
+ raise ScanossCryptographyError(f'Invalid purl format: {purl}')
259
+
260
+ def present(
261
+ self,
262
+ output_format: Optional[str] = None,
263
+ output_file: Optional[str] = None,
264
+ ):
265
+ """Present the results in the selected format"""
266
+ self.presenter.present(output_format=output_format, output_file=output_file)
267
+
268
+
269
+ class CryptographyPresenter(AbstractPresenter):
270
+ """
271
+ Cryptography presenter class
272
+ Handles the presentation of the cryptography results
273
+ """
274
+
275
+ def __init__(self, cryptography: Cryptography, **kwargs):
276
+ super().__init__(**kwargs)
277
+ self.cryptography = cryptography
278
+
279
+ def _format_json_output(self) -> str:
280
+ """
281
+ Format the scan output data into a JSON object
282
+
283
+ Returns:
284
+ str: The formatted JSON string
285
+ """
286
+ return json.dumps(self.cryptography.results, indent=2)
287
+
288
+ def _format_plain_output(self) -> str:
289
+ """
290
+ Format the scan output data into a plain text string
291
+ """
292
+ return (
293
+ json.dumps(self.cryptography.results, indent=2)
294
+ if isinstance(self.cryptography.results, dict)
295
+ else str(self.cryptography.results)
296
+ )
297
+
298
+ def _format_cyclonedx_output(self) -> str:
299
+ raise NotImplementedError('CycloneDX output is not implemented')
300
+
301
+ def _format_spdxlite_output(self) -> str:
302
+ raise NotImplementedError('SPDXlite output is not implemented')
303
+
304
+ def _format_csv_output(self) -> str:
305
+ raise NotImplementedError('CSV output is not implemented')
306
+
307
+ def _format_raw_output(self) -> str:
308
+ raise NotImplementedError('Raw output is not implemented')
scanoss/csvoutput.py CHANGED
@@ -1,30 +1,30 @@
1
1
  """
2
- SPDX-License-Identifier: MIT
2
+ SPDX-License-Identifier: MIT
3
3
 
4
- Copyright (c) 2022, SCANOSS
4
+ Copyright (c) 2022, SCANOSS
5
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:
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
12
 
13
- The above copyright notice and this permission notice shall be included in
14
- all copies or substantial portions of the Software.
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
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.
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
23
  """
24
+ import csv
24
25
  import json
25
26
  import os.path
26
27
  import sys
27
- import csv
28
28
 
29
29
  from .scanossbase import ScanossBase
30
30
 
@@ -43,37 +43,41 @@ class CsvOutput(ScanossBase):
43
43
  self.output_file = output_file
44
44
  self.debug = debug
45
45
 
46
- def parse(self, data: json):
46
+ # TODO Refactor (fails linter)
47
+ def parse(self, data: json): #noqa PLR0912, PLR0915
47
48
  """
48
49
  Parse the given input (raw/plain) JSON string and return CSV summary
49
50
  :param data: json - JSON object
50
51
  :return: CSV dictionary
51
52
  """
52
- if not data:
53
+ if data is None:
53
54
  self.print_stderr('ERROR: No JSON data provided to parse.')
54
55
  return None
55
- self.print_debug(f'Processing raw results into CSV format...')
56
+ if len(data) == 0:
57
+ self.print_msg('Warning: Empty scan results provided. Returning empty CSV list.')
58
+ return []
59
+ self.print_debug('Processing raw results into CSV format...')
56
60
  csv_dict = []
57
61
  row_id = 1
58
62
  for f in data:
59
63
  file_details = data.get(f)
60
64
  # print(f'File: {f}: {file_details}')
61
65
  for d in file_details:
62
- id_details = d.get("id")
66
+ id_details = d.get('id')
63
67
  if not id_details or id_details == 'none':
64
68
  continue
65
- matched = d.get("matched", '')
66
- lines = d.get("lines", '').replace(',', ';') # swap comma with semicolon to help basic parsers
67
- oss_lines = d.get("oss_lines", '').replace(',', ';')
69
+ matched = d.get('matched', '')
70
+ lines = d.get('lines', '').replace(',', ';') # swap comma with semicolon to help basic parsers
71
+ oss_lines = d.get('oss_lines', '').replace(',', ';')
68
72
  detected = {}
69
73
  if id_details == 'dependency':
70
- dependencies = d.get("dependencies")
74
+ dependencies = d.get('dependencies')
71
75
  if not dependencies:
72
76
  self.print_stderr(f'Warning: No Dependencies found for {f}: {file_details}')
73
77
  continue
74
78
  for deps in dependencies:
75
79
  detected = {}
76
- purl = deps.get("purl")
80
+ purl = deps.get('purl')
77
81
  if not purl:
78
82
  self.print_stderr(f'Warning: No PURL found for {f}: {deps}')
79
83
  continue
@@ -84,25 +88,32 @@ class CsvOutput(ScanossBase):
84
88
  dc = []
85
89
  if licenses:
86
90
  for lic in licenses:
87
- name = lic.get("name")
91
+ name = lic.get('name')
88
92
  if name and name not in dc: # Only save the license name once
89
93
  dc.append(name)
90
94
  if not dc or len(dc) == 0:
91
95
  detected['licenses'] = ''
92
96
  else:
93
97
  detected['licenses'] = ';'.join(dc)
94
- # inventory_id,path,usage,detected_component,detected_license,detected_version,detected_latest,purl
95
- csv_dict.append({'inventory_id': row_id, 'path': f, 'detected_usage': id_details,
96
- 'detected_component': detected.get('component'),
97
- 'detected_license': detected.get('licenses'),
98
- 'detected_version': detected.get('version'),
99
- 'detected_latest': detected.get('latest'),
100
- 'detected_purls': detected.get('purls'),
101
- 'detected_url': detected.get('url'),
102
- 'detected_path': detected.get('file', ''),
103
- 'detected_match': matched, 'detected_lines': lines,
104
- 'detected_oss_lines': oss_lines
105
- })
98
+ # inventory_id,path,usage,detected_component,detected_license,
99
+ # detected_version,detected_latest,purl
100
+ csv_dict.append(
101
+ {
102
+ 'inventory_id': row_id,
103
+ 'path': f,
104
+ 'detected_usage': id_details,
105
+ 'detected_component': detected.get('component'),
106
+ 'detected_license': detected.get('licenses'),
107
+ 'detected_version': detected.get('version'),
108
+ 'detected_latest': detected.get('latest'),
109
+ 'detected_purls': detected.get('purls'),
110
+ 'detected_url': detected.get('url'),
111
+ 'detected_path': detected.get('file', ''),
112
+ 'detected_match': matched,
113
+ 'detected_lines': lines,
114
+ 'detected_oss_lines': oss_lines,
115
+ }
116
+ )
106
117
  row_id = row_id + 1
107
118
  else:
108
119
  purls = d.get('purl')
@@ -123,25 +134,31 @@ class CsvOutput(ScanossBase):
123
134
  dc = []
124
135
  if licenses:
125
136
  for lic in licenses:
126
- name = lic.get("name")
137
+ name = lic.get('name')
127
138
  if name and name not in dc: # Only save the license name once
128
- dc.append(lic.get("name"))
139
+ dc.append(lic.get('name'))
129
140
  if not dc or len(dc) == 0:
130
141
  detected['licenses'] = ''
131
142
  else:
132
143
  detected['licenses'] = ';'.join(dc)
133
144
  # inventory_id,path,usage,detected_component,detected_license,detected_version,detected_latest,purl
134
- csv_dict.append({'inventory_id': row_id, 'path': f, 'detected_usage': id_details,
135
- 'detected_component': detected.get('component'),
136
- 'detected_license': detected.get('licenses'),
137
- 'detected_version': detected.get('version'),
138
- 'detected_latest': detected.get('latest'),
139
- 'detected_purls': detected.get('purls'),
140
- 'detected_url': detected.get('url'),
141
- 'detected_path': detected.get('file', ''),
142
- 'detected_match': matched, 'detected_lines': lines,
143
- 'detected_oss_lines': oss_lines
144
- })
145
+ csv_dict.append(
146
+ {
147
+ 'inventory_id': row_id,
148
+ 'path': f,
149
+ 'detected_usage': id_details,
150
+ 'detected_component': detected.get('component'),
151
+ 'detected_license': detected.get('licenses'),
152
+ 'detected_version': detected.get('version'),
153
+ 'detected_latest': detected.get('latest'),
154
+ 'detected_purls': detected.get('purls'),
155
+ 'detected_url': detected.get('url'),
156
+ 'detected_path': detected.get('file', ''),
157
+ 'detected_match': matched,
158
+ 'detected_lines': lines,
159
+ 'detected_oss_lines': oss_lines,
160
+ }
161
+ )
145
162
  row_id = row_id + 1
146
163
  return csv_dict
147
164
 
@@ -170,20 +187,34 @@ class CsvOutput(ScanossBase):
170
187
  :return: True if successful, False otherwise
171
188
  """
172
189
  csv_data = self.parse(data)
173
- if not csv_data:
190
+ if csv_data is None:
174
191
  self.print_stderr('ERROR: No CSV data returned for the JSON string provided.')
175
192
  return False
193
+ if len(csv_data) == 0:
194
+ self.print_msg('Warning: Empty scan results - generating CSV with headers only.')
176
195
  # Header row/column details
177
- fields = ['inventory_id', 'path', 'detected_usage', 'detected_component', 'detected_license',
178
- 'detected_version', 'detected_latest', 'detected_purls', 'detected_url', 'detected_match',
179
- 'detected_lines', 'detected_oss_lines', 'detected_path']
196
+ fields = [
197
+ 'inventory_id',
198
+ 'path',
199
+ 'detected_usage',
200
+ 'detected_component',
201
+ 'detected_license',
202
+ 'detected_version',
203
+ 'detected_latest',
204
+ 'detected_purls',
205
+ 'detected_url',
206
+ 'detected_match',
207
+ 'detected_lines',
208
+ 'detected_oss_lines',
209
+ 'detected_path',
210
+ ]
180
211
  file = sys.stdout
181
212
  if not output_file and self.output_file:
182
213
  output_file = self.output_file
183
214
  if output_file:
184
215
  file = open(output_file, 'w')
185
216
  writer = csv.DictWriter(file, fieldnames=fields)
186
- writer.writeheader() # writing headers (field names)
217
+ writer.writeheader() # writing headers (field names)
187
218
  writer.writerows(csv_data) # writing data rows
188
219
  if output_file:
189
220
  file.close()
@@ -206,6 +237,8 @@ class CsvOutput(ScanossBase):
206
237
  self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
207
238
  return False
208
239
  return self.produce_from_json(data, output_file)
240
+
241
+
209
242
  #
210
243
  # End of CsvOutput Class
211
244
  #