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,479 @@
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
+ import json
25
+ import time
26
+ from datetime import datetime
27
+ from typing import Any, Dict, List, Optional, TypedDict
28
+
29
+ from ....services.dependency_track_service import DependencyTrackService
30
+ from ...utils.markdown_utils import generate_jira_table, generate_table
31
+ from ..policy_check import PolicyCheck, PolicyOutput, PolicyStatus
32
+
33
+ # Constants
34
+ PROCESSING_RETRY_DELAY = 5 # seconds
35
+ DEFAULT_TIME_OUT = 300.0
36
+ MILLISECONDS_TO_SECONDS = 1000
37
+
38
+ """
39
+ Dependency Track project violation policy check implementation.
40
+
41
+ This module provides policy checking functionality for Dependency Track project violations.
42
+ It retrieves, processes, and formats policy violations from a Dependency Track instance
43
+ for a specific project.
44
+ """
45
+
46
+
47
+ class ResolvedLicenseDict(TypedDict):
48
+ """TypedDict for resolved license information from Dependency Track."""
49
+ uuid: str
50
+ name: str
51
+ licenseId: str
52
+ isOsiApproved: bool
53
+ isFsfLibre: bool
54
+ isDeprecatedLicenseId: bool
55
+ isCustomLicense: bool
56
+
57
+
58
+ class ProjectDict(TypedDict):
59
+ """TypedDict for project information from Dependency Track."""
60
+ authors: List[str]
61
+ name: str
62
+ version: str
63
+ classifier: str
64
+ collectionLogic: str
65
+ uuid: str
66
+ properties: List[Any]
67
+ tags: List[str]
68
+ lastBomImport: int
69
+ lastBomImportFormat: str
70
+ lastInheritedRiskScore: float
71
+ lastVulnerabilityAnalysis: int
72
+ active: bool
73
+ isLatest: bool
74
+
75
+
76
+ class ComponentDict(TypedDict):
77
+ """TypedDict for component information from Dependency Track."""
78
+ authors: List[str]
79
+ name: str
80
+ version: str
81
+ classifier: str
82
+ purl: str
83
+ purlCoordinates: str
84
+ resolvedLicense: ResolvedLicenseDict
85
+ project: ProjectDict
86
+ lastInheritedRiskScore: float
87
+ uuid: str
88
+ expandDependencyGraph: bool
89
+ isInternal: bool
90
+ cpe: Optional[str]
91
+
92
+
93
+ class PolicyDict(TypedDict):
94
+ """TypedDict for policy information from Dependency Track."""
95
+ name: str
96
+ operator: str
97
+ violationState: str
98
+ uuid: str
99
+ includeChildren: bool
100
+ onlyLatestProjectVersion: bool
101
+
102
+
103
+ class PolicyConditionDict(TypedDict):
104
+ """TypedDict for policy condition information from Dependency Track."""
105
+ policy: PolicyDict
106
+ operator: str
107
+ subject: str
108
+ value: str
109
+ uuid: str
110
+
111
+
112
+ class PolicyViolationDict(TypedDict):
113
+ """TypedDict for policy violation information from Dependency Track."""
114
+ type: str
115
+ project: ProjectDict
116
+ component: ComponentDict
117
+ policyCondition: PolicyConditionDict
118
+ timestamp: int
119
+ uuid: str
120
+
121
+
122
+ class DependencyTrackProjectViolationPolicyCheck(PolicyCheck[PolicyViolationDict]):
123
+ """
124
+ Policy check implementation for Dependency Track project violations.
125
+
126
+ This class handles retrieving, processing, and formatting policy violations
127
+ from a Dependency Track instance for a specific project.
128
+ """
129
+
130
+ def __init__( # noqa: PLR0913
131
+ self,
132
+ debug: bool = False,
133
+ trace: bool = False,
134
+ quiet: bool = False,
135
+ project_id: str = None,
136
+ project_name: str = None,
137
+ project_version: str = None,
138
+ api_key: str = None,
139
+ url: str = None,
140
+ upload_token: str = None,
141
+ timeout: float = DEFAULT_TIME_OUT,
142
+ format_type: str = None,
143
+ status: str = None,
144
+ output: str = None,
145
+ ):
146
+ """
147
+ Initialise the Dependency Track project violation policy checker.
148
+
149
+ Args:
150
+ debug: Enable debug output
151
+ trace: Enable trace output
152
+ quiet: Enable quiet mode
153
+ project_id: UUID of the project in Dependency Track
154
+ project_name: Name of the project in Dependency Track
155
+ project_version: Version of the project in Dependency Track
156
+ api_key: API key for Dependency Track authentication
157
+ url: Base URL of the Dependency Track instance
158
+ upload_token: Upload token for uploading BOMs to Dependency Track
159
+ format_type: Output format type (json, markdown, etc.)
160
+ status: Status output destination
161
+ output: Results output destination
162
+ timeout: Timeout for processing in seconds (default: 300)
163
+ """
164
+ super().__init__(debug, trace, quiet, format_type, status, 'dependency-track', output)
165
+ self.api_key = api_key
166
+ self.project_id = project_id
167
+ self.project_name = project_name
168
+ self.project_version = project_version
169
+ self.upload_token = upload_token
170
+ self.timeout = timeout
171
+ self.url = url.strip().rstrip('/') if url else None
172
+ self.dep_track_service = DependencyTrackService(self.api_key, self.url, debug=debug, trace=trace, quiet=quiet)
173
+
174
+ def _json(self, project_violations: list[PolicyViolationDict]) -> PolicyOutput:
175
+ """
176
+ Format project violations as JSON.
177
+
178
+ Args:
179
+ project_violations: List of policy violations from Dependency Track
180
+
181
+ Returns:
182
+ Dictionary containing JSON formatted results and summary
183
+ """
184
+ return PolicyOutput(
185
+ details= json.dumps(project_violations, indent=2),
186
+ summary= f'{len(project_violations)} policy violations were found.\n',
187
+ )
188
+
189
+ def _markdown(self, project_violations: list[PolicyViolationDict]) -> PolicyOutput:
190
+ """
191
+ Format Dependency Track violations to Markdown format.
192
+
193
+ Args:
194
+ project_violations: List of policy violations from Dependency Track
195
+
196
+ Returns:
197
+ Dictionary with formatted Markdown details and summary
198
+ """
199
+ return self._md_summary_generator(project_violations, generate_table)
200
+
201
+ def _jira_markdown(self, data: list[PolicyViolationDict]) -> PolicyOutput:
202
+ """
203
+ Format project violations for Jira Markdown.
204
+
205
+ Args:
206
+ data: List of policy violations from Dependency Track
207
+
208
+ Returns:
209
+ Dictionary containing Jira markdown formatted results and summary
210
+ """
211
+ return self._md_summary_generator(data, generate_jira_table)
212
+
213
+ def is_project_updated(self, dt_project: Dict[str, Any]) -> bool:
214
+ """
215
+ Check if a Dependency Track project has completed processing.
216
+
217
+ This method determines if a project has finished processing by comparing
218
+ the timestamps of the last BOM import, vulnerability analysis, and last
219
+ occurrence metrics. A project is considered updated when either the
220
+ vulnerability analysis or the metrics' last occurrence timestamp is greater
221
+ than or equal to the last BOM import timestamp.
222
+
223
+ Args:
224
+ dt_project: Project dictionary from Dependency Track containing
225
+ project metadata and timestamps
226
+
227
+ Returns:
228
+ True if the project has completed processing (vulnerability analysis
229
+ or metrics are up to date with the last BOM import), False otherwise
230
+ """
231
+ if not dt_project:
232
+ self.print_stderr('Warning: No project details supplied. Returning False.')
233
+ return False
234
+
235
+ # Safely extract and normalise timestamp values to numeric types
236
+ def _safe_timestamp(field, value=None, default=0) -> float:
237
+ """Convert timestamp value to float, handling string/numeric types safely."""
238
+ if value is None:
239
+ return float(default)
240
+ try:
241
+ return float(value)
242
+ except (ValueError, TypeError):
243
+ self.print_stderr(f'Warning: Invalid timestamp for {field}, value: {value}, using default: {default}')
244
+ return float(default)
245
+
246
+ last_import = _safe_timestamp('lastBomImport', dt_project.get('lastBomImport'), 0)
247
+ last_vulnerability_analysis = _safe_timestamp('lastVulnerabilityAnalysis',
248
+ dt_project.get('lastVulnerabilityAnalysis'), 0
249
+ )
250
+ metrics = dt_project.get('metrics', {})
251
+ last_occurrence = _safe_timestamp('lastOccurrence',
252
+ metrics.get('lastOccurrence', 0)
253
+ if isinstance(metrics, dict) else 0, 0
254
+ )
255
+ if self.debug:
256
+ self.print_msg(f'last_import: {last_import}')
257
+ self.print_msg(f'last_vulnerability_analysis: {last_vulnerability_analysis}')
258
+ self.print_msg(f'last_occurrence: {last_occurrence}')
259
+ self.print_msg(f'last_vulnerability_analysis is updated: {last_vulnerability_analysis >= last_import}')
260
+ self.print_msg(f'last_occurrence is updated: {last_occurrence >= last_import}')
261
+ # Catches case where vulnerability analysis is skipped for empty SBOMs
262
+ if 0 < last_import <= last_occurrence:
263
+ component_count = metrics.get('components', 0) if isinstance(metrics, dict) else 0
264
+ if component_count < 1:
265
+ self.print_msg('Notice: Empty SBOM detected. Assuming no violations.')
266
+ return True
267
+ # If all timestamps are zero, this indicates no processing has occurred
268
+ if last_vulnerability_analysis == 0 or last_occurrence == 0 or last_import == 0:
269
+ self.print_stderr(f'Warning: Some project data appears to be unset. Returning False: {dt_project}')
270
+ return False
271
+ # True if: Both vulnerability analysis and metrics calculation newer than last BOM upload
272
+ return last_vulnerability_analysis >= last_import and last_occurrence >= last_import
273
+
274
+ def _wait_processing_by_project_id(self) -> Optional[Any] or None:
275
+ """
276
+ Wait for project processing to complete in Dependency Track.
277
+
278
+ Returns:
279
+ Return the project or None if processing fails or times out
280
+ """
281
+ start_time = time.time()
282
+ while True:
283
+ self.print_debug('Starting...')
284
+ dt_project = self.dep_track_service.get_project_by_id(self.project_id)
285
+ if not dt_project:
286
+ self.print_stderr(f'Failed to get project by id: {self.project_id}')
287
+ return None
288
+ is_project_updated = self.is_project_updated(dt_project)
289
+ if is_project_updated: # Project updated, return it
290
+ return dt_project
291
+ # Check timeout
292
+ if time.time() - start_time > self.timeout:
293
+ self.print_msg(f'Warning: Timeout reached ({self.timeout}s) while waiting for project processing')
294
+ return dt_project
295
+ time.sleep(PROCESSING_RETRY_DELAY)
296
+ self.print_debug('Checking if complete...')
297
+ # End while loop
298
+
299
+ def _wait_processing_by_project_status(self):
300
+ """
301
+ Wait for project processing to complete in Dependency Track.
302
+
303
+ Returns:
304
+ Project status dictionary or None if processing fails or times out
305
+ """
306
+ start_time = time.time()
307
+ while True:
308
+ status = self.dep_track_service.get_project_status(self.upload_token)
309
+ if status is None:
310
+ self.print_stderr(f'Error getting project status for upload token: {self.upload_token}')
311
+ break
312
+ if status and not status.get('processing'):
313
+ self.print_debug(f'Project Status: {status}')
314
+ break
315
+ if time.time() - start_time > self.timeout:
316
+ self.print_msg(f'Timeout reached ({self.timeout}s) while waiting for project processing')
317
+ break
318
+ time.sleep(PROCESSING_RETRY_DELAY)
319
+ self.print_debug('Checking if complete...')
320
+ # End while loop
321
+
322
+ def _wait_project_processing(self):
323
+ """
324
+ Wait for project processing to complete in Dependency Track.
325
+
326
+ Returns:
327
+ Project status dictionary or None if processing fails
328
+ """
329
+ if self.upload_token:
330
+ self.print_debug("Using upload token to check project status")
331
+ self._wait_processing_by_project_status()
332
+ self.print_debug("Using project id to get project status")
333
+ return self._wait_processing_by_project_id()
334
+
335
+ def _set_project_id(self) -> None:
336
+ """
337
+ Set the project ID based on the project name and version if not already set.
338
+ If no project id is specified, this method will attempt to retrieve the project based on name/version.
339
+
340
+ Raises:
341
+ ValueError: If the project name/version is missing or the project is not found.
342
+ RuntimeError: If there's an error communicating with Dependency Track.
343
+ """
344
+ if self.project_id is not None:
345
+ return
346
+ if self.project_name is None or self.project_version is None:
347
+ raise ValueError(
348
+ "Error: Project name and version must be specified when not using project ID"
349
+ )
350
+ self.print_debug(f'Searching for project id by name and version: {self.project_name}@{self.project_version}')
351
+ dt_project = self.dep_track_service.get_project_by_name_version(self.project_name, self.project_version)
352
+ self.print_debug(f'dt_project: {dt_project}')
353
+ if dt_project is None:
354
+ raise ValueError(f'Error: Project {self.project_name}@{self.project_version} not found in Dependency Track')
355
+ self.project_id = dt_project.get('uuid')
356
+ if not self.project_id:
357
+ self.print_stderr(f'Error: Failed to get project uuid from: {dt_project}')
358
+ raise ValueError(f'Error: Project {self.project_name}@{self.project_version} does not have a valid UUID')
359
+
360
+ def _sort_project_violations(self,violations: List[PolicyViolationDict]) -> List[PolicyViolationDict]:
361
+ """
362
+ Sort project violations by priority.
363
+
364
+ Sorts violations with SECURITY issues first, followed by LICENSE,
365
+ then OTHER types.
366
+
367
+ Args:
368
+ violations: List of policy violation dictionaries
369
+
370
+ Returns:
371
+ Sorted list of policy violations
372
+ """
373
+ type_priority = {'SECURITY': 3, 'LICENSE': 2, 'OTHER': 1}
374
+ return sorted(
375
+ violations,
376
+ key=lambda x: -type_priority.get(x.get('type', 'OTHER'), 1)
377
+ )
378
+
379
+ def _md_summary_generator(self, project_violations: list[PolicyViolationDict], table_generator) -> PolicyOutput:
380
+ """
381
+ Generates a Markdown summary of project policy violations.
382
+
383
+ Args:
384
+ project_violations (list[PolicyViolationDict]): A list of dictionaries containing details of
385
+ project policy violations, including violation state, risk type, policy name, component details,
386
+ and timestamp.
387
+ table_generator (function): A callable function responsible for generating the Markdown table
388
+ using headers, rows, and optionally highlighted columns.
389
+
390
+ Returns:
391
+ dict: A dictionary with two keys:
392
+ - "details" containing a Markdown-compatible string with detailed project violations
393
+ rendered as a table
394
+ - "summary" summarising the number of violations found
395
+ """
396
+ if project_violations is None:
397
+ self.print_stderr('Warning: No project violations found. Returning empty results.')
398
+ return PolicyOutput(
399
+ details= "h3. Dependency Track Project Violations\n\nNo policy violations found.\n",
400
+ summary= "0 policy violations were found.\n",
401
+ )
402
+ headers = ['State', 'Risk Type', 'Policy Name', 'Component', 'Date']
403
+ c_cols = [0, 1]
404
+ rows: List[List[str]] = []
405
+
406
+ for project_violation in project_violations:
407
+ timestamp = project_violation['timestamp']
408
+ timestamp_seconds = timestamp / MILLISECONDS_TO_SECONDS
409
+ formatted_date = datetime.fromtimestamp(timestamp_seconds).strftime("%d %b %Y at %H:%M:%S")
410
+
411
+ purl = project_violation["component"]["purl"]
412
+ version = project_violation["component"]["version"]
413
+ # If PURL doesn't contain version but version is available, append it
414
+ component_display = purl
415
+ if version and '@' not in purl:
416
+ component_display = f'{purl}@{version}'
417
+ row = [
418
+ project_violation['policyCondition']["policy"]["violationState"],
419
+ project_violation['type'],
420
+ project_violation['policyCondition']["policy"]["name"],
421
+ component_display,
422
+ formatted_date,
423
+ ]
424
+ rows.append(row)
425
+ # End for loop
426
+ return PolicyOutput(
427
+ details= f'### Dependency Track Project Violations\n{table_generator(headers, rows, c_cols)}\n\n'
428
+ f'View project in Dependency Track [here]({self.url}/projects/{self.project_id}).\n',
429
+ summary= f'{len(project_violations)} policy violations were found.\n'
430
+ )
431
+
432
+ def run(self) -> int:
433
+ """
434
+ Runs the primary execution logic of the instance.
435
+
436
+ Returns:
437
+ int: Status code indicating the result of the run process. Possible
438
+ values are derived from the PolicyStatus enumeration.
439
+ FAIL if violations are found, SUCCESS if no violations are found, ERROR if an error occurs.
440
+
441
+ Raises:
442
+ ValueError: If an invalid format is specified during the execution.
443
+ """
444
+ # Set project ID based on name/version if needed
445
+ self._set_project_id()
446
+ if self.debug:
447
+ self.print_msg(f'URL: {self.url}')
448
+ self.print_msg(f'Project Id: {self.project_id}')
449
+ self.print_msg(f'Project Name: {self.project_name}')
450
+ self.print_msg(f'Project Version: {self.project_version}')
451
+ self.print_msg(f'API Key: {"*" * len(self.api_key)}')
452
+ self.print_msg(f'Format: {self.format_type}')
453
+ self.print_msg(f'Status: {self.status}')
454
+ self.print_msg(f'Output: {self.output}')
455
+ self.print_msg(f'Timeout: {self.timeout}')
456
+ # Confirm processing is complete before returning project violations
457
+ dt_project = self._wait_project_processing()
458
+ if not dt_project:
459
+ return PolicyStatus.ERROR.value
460
+ # Get project violations from Dependency Track
461
+ dt_project_violations = self.dep_track_service.get_project_violations(self.project_id)
462
+ # Handle case where service returns None (API error) vs empty list (no violations)
463
+ if dt_project_violations is None:
464
+ self.print_stderr('Error: Failed to retrieve project violations from Dependency Track')
465
+ return PolicyStatus.ERROR.value
466
+ # Sort violations by priority and format output
467
+ formatter = self._get_formatter()
468
+ if formatter is None:
469
+ self.print_stderr('Error: Invalid format specified.')
470
+ return PolicyStatus.ERROR.value
471
+ # Format and output data - handle empty results gracefully
472
+ policy_output = formatter(self._sort_project_violations(dt_project_violations))
473
+ self.print_to_file_or_stdout(policy_output.details, self.output)
474
+ self.print_to_file_or_stderr(policy_output.summary, self.status)
475
+ # Return appropriate status based on violation count
476
+ if len(dt_project_violations) > 0:
477
+ return PolicyStatus.POLICY_FAIL.value
478
+ return PolicyStatus.POLICY_SUCCESS.value
479
+