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
scanoss/cli.py CHANGED
@@ -1,42 +1,96 @@
1
1
  """
2
- SPDX-License-Identifier: MIT
3
-
4
- Copyright (c) 2021, 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.
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2021, 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
23
  """
24
+
24
25
  import argparse
25
26
  import os
26
27
  import sys
28
+ import traceback
29
+ from dataclasses import asdict
30
+ from pathlib import Path
31
+ from typing import List
27
32
 
28
33
  import pypac
29
34
 
30
- from .scanner import Scanner
35
+ from scanoss.cryptography import Cryptography, create_cryptography_config_from_args
36
+ from scanoss.delta import Delta
37
+ from scanoss.export.dependency_track import DependencyTrackExporter
38
+ from scanoss.scanners.container_scanner import (
39
+ DEFAULT_SYFT_COMMAND,
40
+ DEFAULT_SYFT_TIMEOUT,
41
+ ContainerScanner,
42
+ create_container_scanner_config_from_args,
43
+ )
44
+ from scanoss.scanners.folder_hasher import (
45
+ FolderHasher,
46
+ create_folder_hasher_config_from_args,
47
+ )
48
+ from scanoss.scanossgrpc import (
49
+ ScanossGrpc,
50
+ ScanossGrpcError,
51
+ create_grpc_config_from_args,
52
+ )
53
+
54
+ from . import __version__
55
+ from .components import Components
56
+ from .constants import (
57
+ DEFAULT_API_TIMEOUT,
58
+ DEFAULT_COPYLEFT_LICENSE_SOURCES,
59
+ DEFAULT_HFH_DEPTH,
60
+ DEFAULT_HFH_MIN_ACCEPTED_SCORE,
61
+ DEFAULT_HFH_RANK_THRESHOLD,
62
+ DEFAULT_HFH_RECURSIVE_THRESHOLD,
63
+ DEFAULT_POST_SIZE,
64
+ DEFAULT_RETRY,
65
+ DEFAULT_TIMEOUT,
66
+ MIN_TIMEOUT,
67
+ PYTHON_MAJOR_VERSION,
68
+ VALID_LICENSE_SOURCES,
69
+ )
70
+ from .csvoutput import CsvOutput
71
+ from .cyclonedx import CycloneDx
72
+ from .filecount import FileCount
73
+ from .gitlabqualityreport import GitLabQualityReport
74
+ from .inspection.policy_check.dependency_track.project_violation import (
75
+ DependencyTrackProjectViolationPolicyCheck,
76
+ )
77
+ from .inspection.policy_check.scanoss.copyleft import Copyleft
78
+ from .inspection.policy_check.scanoss.undeclared_component import UndeclaredComponent
79
+ from .inspection.summary.component_summary import ComponentSummary
80
+ from .inspection.summary.license_summary import LicenseSummary
81
+ from .inspection.summary.match_summary import MatchSummary
82
+ from .results import Results
31
83
  from .scancodedeps import ScancodeDeps
84
+ from .scanner import FAST_WINNOWING, Scanner
85
+ from .scanners.scanner_config import create_scanner_config_from_args
86
+ from .scanners.scanner_hfh import ScannerHFH
87
+ from .scanoss_settings import ScanossSettings, ScanossSettingsError
32
88
  from .scantype import ScanType
33
- from .filecount import FileCount
34
- from .cyclonedx import CycloneDx
35
89
  from .spdxlite import SpdxLite
36
- from .csvoutput import CsvOutput
37
- from .components import Components
38
- from . import __version__
39
- from .scanner import FAST_WINNOWING
90
+ from .threadeddependencies import SCOPE
91
+ from .utils.file import validate_json_file
92
+
93
+ HEADER_PARTS_COUNT = 2
40
94
 
41
95
 
42
96
  def print_stderr(*args, **kwargs):
@@ -46,134 +100,267 @@ def print_stderr(*args, **kwargs):
46
100
  print(*args, file=sys.stderr, **kwargs)
47
101
 
48
102
 
49
- def setup_args() -> None:
103
+ def setup_args() -> None: # noqa: PLR0912, PLR0915
50
104
  """
51
105
  Setup all the command line arguments for processing
52
106
  """
53
- parser = argparse.ArgumentParser(description=f'SCANOSS Python CLI. Ver: {__version__}, License: MIT, Fast Winnowing: {FAST_WINNOWING}')
107
+ parser = argparse.ArgumentParser(
108
+ description=f'SCANOSS Python CLI. Ver: {__version__}, License: MIT, Fast Winnowing: {FAST_WINNOWING}'
109
+ )
54
110
  parser.add_argument('--version', '-v', action='store_true', help='Display version details')
55
111
 
56
- subparsers = parser.add_subparsers(title='Sub Commands', dest='subparser', description='valid subcommands',
57
- help='sub-command help')
112
+ subparsers = parser.add_subparsers(
113
+ title='Sub Commands', dest='subparser', description='valid subcommands', help='sub-command help'
114
+ )
58
115
  # Sub-command: version
59
- p_ver = subparsers.add_parser('version', aliases=['ver'],
60
- description=f'Version of SCANOSS CLI: {__version__}', help='SCANOSS version')
116
+ p_ver = subparsers.add_parser(
117
+ 'version', aliases=['ver'], description=f'Version of SCANOSS CLI: {__version__}', help='SCANOSS version'
118
+ )
61
119
  p_ver.set_defaults(func=ver)
62
120
 
63
121
  # Sub-command: scan
64
- p_scan = subparsers.add_parser('scan', aliases=['sc'],
65
- description=f'Analyse/scan the given source base: {__version__}',
66
- help='Scan source code')
122
+ p_scan = subparsers.add_parser(
123
+ 'scan',
124
+ aliases=['sc'],
125
+ description=f'Analyse/scan the given source base: {__version__}',
126
+ help='Scan source code',
127
+ )
67
128
  p_scan.set_defaults(func=scan)
68
129
  p_scan.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', help='A file or folder to scan')
69
- p_scan.add_argument('--wfp', '-w', type=str,
70
- help='Scan a WFP File instead of a folder (optional)')
71
- p_scan.add_argument('--dep', '-p', type=str,
72
- help='Use a dependency file instead of a folder (optional)')
73
- p_scan.add_argument('--stdin', '-s', metavar='STDIN-FILENAME', type=str,
74
- help='Scan the file contents supplied via STDIN (optional)')
130
+ p_scan.add_argument('--wfp', '-w', type=str, help='Scan a WFP File instead of a folder (optional)')
131
+ p_scan.add_argument('--dep', '-p', type=str, help='Use a dependency file instead of a folder (optional)')
132
+ p_scan.add_argument(
133
+ '--stdin', '-s', metavar='STDIN-FILENAME', type=str, help='Scan the file contents supplied via STDIN (optional)'
134
+ )
135
+ p_scan.add_argument('--files', '-e', type=str, nargs='*', help='List of files to scan.')
75
136
  p_scan.add_argument('--identify', '-i', type=str, help='Scan and identify components in SBOM file')
76
- p_scan.add_argument('--ignore', '-n', type=str, help='Ignore components specified in the SBOM file')
77
- p_scan.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).')
78
- p_scan.add_argument('--format', '-f', type=str, choices=['plain', 'cyclonedx', 'spdxlite', 'csv'],
79
- help='Result output format (optional - default: plain)')
80
- p_scan.add_argument('--threads', '-T', type=int, default=5,
81
- help='Number of threads to use while scanning (optional - default 5)')
82
- p_scan.add_argument('--flags', '-F', type=int,
83
- help='Scanning engine flags (1: disable snippet matching, 2 enable snippet ids, '
84
- '4: disable dependencies, 8: disable licenses, 16: disable copyrights,'
85
- '32: disable vulnerabilities, 64: disable quality, 128: disable cryptography,'
86
- '256: disable best match only, 512: hide identified files, '
87
- '1024: enable download_url, 2048: enable GitHub full path, '
88
- '4096: disable extended server stats)')
89
- p_scan.add_argument('--post-size', '-P', type=int, default=32,
90
- help='Number of kilobytes to limit the post to while scanning (optional - default 32)')
91
- p_scan.add_argument('--timeout', '-M', type=int, default=180,
92
- help='Timeout (in seconds) for API communication (optional - default 180)')
93
- p_scan.add_argument('--retry', '-R', type=int, default=5,
94
- help='Retry limit for API communication (optional - default 5)')
95
- p_scan.add_argument('--no-wfp-output', action='store_true', help='Skip WFP file generation')
137
+ p_scan.add_argument('--ignore', '-n', type=str, help='Ignore components specified in the SBOM file')
138
+ p_scan.add_argument(
139
+ '--threads', '-T', type=int, default=5, help='Number of threads to use while scanning (optional - default 5)'
140
+ )
141
+ p_scan.add_argument(
142
+ '--flags',
143
+ '-F',
144
+ type=int,
145
+ help='Scanning engine flags (1: disable snippet matching, 2 enable snippet ids, '
146
+ '4: disable dependencies, 8: disable licenses, 16: disable copyrights,'
147
+ '32: disable vulnerabilities, 64: disable quality, 128: disable cryptography,'
148
+ '256: disable best match only, 512: hide identified files, '
149
+ '1024: enable download_url, 2048: enable GitHub full path, '
150
+ '4096: disable extended server stats)',
151
+ )
152
+ p_scan.add_argument(
153
+ '--post-size',
154
+ '-P',
155
+ type=int,
156
+ default=DEFAULT_POST_SIZE,
157
+ help='Number of kilobytes to limit the post to while scanning (optional - default 32)',
158
+ )
159
+ p_scan.add_argument(
160
+ '--timeout',
161
+ '-M',
162
+ type=int,
163
+ default=DEFAULT_TIMEOUT,
164
+ help='Timeout (in seconds) for API communication (optional - default 180)',
165
+ )
166
+ p_scan.add_argument(
167
+ '--retry',
168
+ '-R',
169
+ type=int,
170
+ default=DEFAULT_RETRY,
171
+ help='Retry limit for API communication (optional - default 5)',
172
+ )
96
173
  p_scan.add_argument('--dependencies', '-D', action='store_true', help='Add Dependency scanning')
97
174
  p_scan.add_argument('--dependencies-only', action='store_true', help='Run Dependency scanning only')
98
- p_scan.add_argument('--sc-command', type=str,
99
- help='Scancode command and path if required (optional - default scancode).')
100
- p_scan.add_argument('--sc-timeout', type=int, default=600,
101
- help='Timeout (in seconds) for scancode to complete (optional - default 600)')
175
+ p_scan.add_argument(
176
+ '--sc-command', type=str, help='Scancode command and path if required (optional - default scancode).'
177
+ )
178
+ p_scan.add_argument(
179
+ '--sc-timeout',
180
+ type=int,
181
+ default=600,
182
+ help='Timeout (in seconds) for scancode to complete (optional - default 600)',
183
+ )
184
+ p_scan.add_argument(
185
+ '--dep-scope', '-ds', type=SCOPE, help='Filter dependencies by scope - default all (options: dev/prod)'
186
+ )
187
+ p_scan.add_argument('--dep-scope-inc', '-dsi', type=str, help='Include dependencies with declared scopes')
188
+ p_scan.add_argument('--dep-scope-exc', '-dse', type=str, help='Exclude dependencies with declared scopes')
189
+ p_scan.add_argument(
190
+ '--no-wfp-output', action='store_true',
191
+ help='DEPRECATED: Scans no longer generate scanner_output.wfp. Use "fingerprint -o" to create WFP files.'
192
+ )
102
193
 
103
194
  # Sub-command: fingerprint
104
- p_wfp = subparsers.add_parser('fingerprint', aliases=['fp', 'wfp'],
105
- description=f'Fingerprint the given source base: {__version__}',
106
- help='Fingerprint source code')
195
+ p_wfp = subparsers.add_parser(
196
+ 'fingerprint',
197
+ aliases=['fp', 'wfp'],
198
+ description=f'Fingerprint the given source base: {__version__}',
199
+ help='Fingerprint source code',
200
+ )
107
201
  p_wfp.set_defaults(func=wfp)
108
- p_wfp.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?',
109
- help='A file or folder to scan')
110
- p_wfp.add_argument('--stdin', '-s', metavar='STDIN-FILENAME', type=str,
111
- help='Fingerprint the file contents supplied via STDIN (optional)')
112
- p_wfp.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).')
202
+ p_wfp.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', help='A file or folder to scan')
203
+ p_wfp.add_argument(
204
+ '--stdin',
205
+ '-s',
206
+ metavar='STDIN-FILENAME',
207
+ type=str,
208
+ help='Fingerprint the file contents supplied via STDIN (optional)',
209
+ )
113
210
 
114
211
  # Sub-command: dependency
115
- p_dep = subparsers.add_parser('dependencies', aliases=['dp', 'dep'],
116
- description=f'Produce dependency file summary: {__version__}',
117
- help='Scan source code for dependencies, but do not decorate them')
212
+ p_dep = subparsers.add_parser(
213
+ 'dependencies',
214
+ aliases=['dp', 'dep'],
215
+ description=f'Produce dependency file summary: {__version__}',
216
+ help='Scan source code for dependencies, but do not decorate them',
217
+ )
218
+ p_dep.add_argument('scan_loc', metavar='FILE/DIR', type=str, nargs='?', help='A file or folder to scan')
219
+ p_dep.add_argument(
220
+ '--container',
221
+ type=str,
222
+ help='Container image to scan. Supports yourrepo/yourimage:tag, Docker tar, '
223
+ 'OCI tar, OCI directory, SIF Container, or generic filesystem directory.',
224
+ )
225
+ p_dep.add_argument(
226
+ '--sc-command', type=str, help='Scancode command and path if required (optional - default scancode).'
227
+ )
228
+ p_dep.add_argument(
229
+ '--sc-timeout',
230
+ type=int,
231
+ default=600,
232
+ help='Timeout (in seconds) for scancode to complete (optional - default 600)',
233
+ )
118
234
  p_dep.set_defaults(func=dependency)
119
- p_dep.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', help='A file or folder to scan')
120
- p_dep.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).')
121
- p_dep.add_argument('--sc-command', type=str,
122
- help='Scancode command and path if required (optional - default scancode).')
123
- p_dep.add_argument('--sc-timeout', type=int, default=600,
124
- help='Timeout (in seconds) for scancode to complete (optional - default 600)')
235
+
236
+ # Container scan sub-command
237
+ p_cs = subparsers.add_parser(
238
+ 'container-scan',
239
+ aliases=['cs'],
240
+ description=f'Analyse/scan the given container image: {__version__}',
241
+ help='Scan container image',
242
+ )
243
+ p_cs.add_argument(
244
+ 'scan_loc',
245
+ metavar='IMAGE',
246
+ type=str,
247
+ nargs='?',
248
+ help=(
249
+ 'Container image to scan. Supports yourrepo/yourimage:tag, Docker tar, '
250
+ 'OCI tar, OCI directory, SIF Container, or generic filesystem directory.'
251
+ ),
252
+ )
253
+ p_cs.add_argument(
254
+ '--retry',
255
+ '-R',
256
+ type=int,
257
+ default=DEFAULT_RETRY,
258
+ help='Retry limit for API communication (optional - default 5)',
259
+ )
260
+ p_cs.add_argument(
261
+ '--timeout',
262
+ '-M',
263
+ type=int,
264
+ default=DEFAULT_TIMEOUT,
265
+ help='Timeout (in seconds) for API communication (optional - default 180)',
266
+ )
267
+ p_cs.set_defaults(func=container_scan)
125
268
 
126
269
  # Sub-command: file_count
127
- p_fc = subparsers.add_parser('file_count', aliases=['fc'],
128
- description=f'Produce a file type count summary: {__version__}',
129
- help='Search the source tree and produce a file type summary')
270
+ p_fc = subparsers.add_parser(
271
+ 'file_count',
272
+ aliases=['fc'],
273
+ description=f'Produce a file type count summary: {__version__}',
274
+ help='Search the source tree and produce a file type summary',
275
+ )
130
276
  p_fc.set_defaults(func=file_count)
131
277
  p_fc.add_argument('scan_dir', metavar='DIR', type=str, nargs='?', help='A folder to search')
132
- p_fc.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).')
133
278
  p_fc.add_argument('--all-hidden', action='store_true', help='Scan all hidden files/folders')
134
279
 
135
280
  # Sub-command: convert
136
- p_cnv = subparsers.add_parser('convert', aliases=['cv', 'cnv', 'cvrt'],
137
- description=f'Convert results files between formats: {__version__}',
138
- help='Convert file format')
281
+ p_cnv = subparsers.add_parser(
282
+ 'convert',
283
+ aliases=['cv', 'cnv', 'cvrt'],
284
+ description=f'Convert results files between formats: {__version__}',
285
+ help='Convert file format',
286
+ )
139
287
  p_cnv.set_defaults(func=convert)
140
288
  p_cnv.add_argument('--input', '-i', type=str, required=True, help='Input file name')
141
- p_cnv.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).')
142
- p_cnv.add_argument('--format', '-f', type=str, choices=['cyclonedx', 'spdxlite', 'csv'], default='spdxlite',
143
- help='Output format (optional - default: spdxlite)')
144
- p_cnv.add_argument('--input-format', type=str, choices=['plain'], default='plain',
145
- help='Input format (optional - default: plain)')
289
+ p_cnv.add_argument(
290
+ '--format',
291
+ '-f',
292
+ type=str,
293
+ choices=['cyclonedx', 'spdxlite', 'csv', 'glc-codequality'],
294
+ default='spdxlite',
295
+ help='Output format (optional - default: spdxlite)',
296
+ )
297
+ p_cnv.add_argument(
298
+ '--input-format', type=str, choices=['plain'], default='plain', help='Input format (optional - default: plain)'
299
+ )
146
300
 
147
301
  # Sub-command: component
148
- p_comp = subparsers.add_parser('component', aliases=['comp'],
149
- description=f'SCANOSS Component commands: {__version__}',
150
- help='Component support commands')
151
-
152
- comp_sub = p_comp.add_subparsers(title='Component Commands', dest='subparsercmd', description='component sub-commands',
153
- help='component sub-commands')
154
-
155
- # Component Sub-command: component crypto
156
- c_crypto = comp_sub.add_parser('crypto', aliases=['cr'],
157
- description=f'Show Cryptographic algorithms: {__version__}',
158
- help='Retreive cryptographic algorithms for the given components')
159
- c_crypto.set_defaults(func=comp_crypto)
302
+ p_comp = subparsers.add_parser(
303
+ 'component',
304
+ aliases=['comp'],
305
+ description=f'SCANOSS Component commands: {__version__}',
306
+ help='Component support commands',
307
+ )
308
+
309
+ comp_sub = p_comp.add_subparsers(
310
+ title='Component Commands',
311
+ dest='subparsercmd',
312
+ description='component sub-commands',
313
+ help='component sub-commands',
314
+ )
160
315
 
161
316
  # Component Sub-command: component vulns
162
- c_vulns = comp_sub.add_parser('vulns', aliases=['vulnerabilities', 'vu'],
163
- description=f'Show Vulnerability details: {__version__}',
164
- help='Retreive vulnerabilities for the given components')
317
+ c_vulns = comp_sub.add_parser(
318
+ 'vulns',
319
+ aliases=['vulnerabilities', 'vu'],
320
+ description=f'Show Vulnerability details: {__version__}',
321
+ help='Retrieve vulnerabilities for the given components',
322
+ )
165
323
  c_vulns.set_defaults(func=comp_vulns)
166
324
 
325
+ # Component Sub-command: component licenses
326
+ c_licenses = comp_sub.add_parser(
327
+ 'licenses',
328
+ aliases=['lics'],
329
+ description=f'Show License details: {__version__}',
330
+ help='Retrieve licenses for the given components',
331
+ )
332
+ c_licenses.set_defaults(func=comp_licenses)
333
+
167
334
  # Component Sub-command: component semgrep
168
- c_semgrep = comp_sub.add_parser('semgrep', aliases=['sp'],
169
- description=f'Show Semgrep findings: {__version__}',
170
- help='Retreive semgrep issues/findings for the given components')
335
+ c_semgrep = comp_sub.add_parser(
336
+ 'semgrep',
337
+ aliases=['sp'],
338
+ description=f'Show Semgrep findings: {__version__}',
339
+ help='Retrieve semgrep issues/findings for the given components',
340
+ )
171
341
  c_semgrep.set_defaults(func=comp_semgrep)
172
342
 
343
+ # Component Sub-command: component provenance
344
+ c_provenance = comp_sub.add_parser(
345
+ 'provenance',
346
+ aliases=['prov', 'prv'],
347
+ description=f'Show GEO Provenance findings: {__version__}',
348
+ help='Retrieve geoprovenance for the given components',
349
+ )
350
+ c_provenance.add_argument(
351
+ '--origin',
352
+ action='store_true',
353
+ help='Retrieve geoprovenance using contributors origin (default: declared origin)',
354
+ )
355
+ c_provenance.set_defaults(func=comp_provenance)
356
+
173
357
  # Component Sub-command: component search
174
- c_search = comp_sub.add_parser('search', aliases=['sc'],
175
- description=f'Search component details: {__version__}',
176
- help='Search for a KB component')
358
+ c_search = comp_sub.add_parser(
359
+ 'search',
360
+ aliases=['sc'],
361
+ description=f'Search component details: {__version__}',
362
+ help='Search for a KB component',
363
+ )
177
364
  c_search.add_argument('--input', '-i', type=str, help='Input file name')
178
365
  c_search.add_argument('--search', '-s', type=str, help='Generic component search')
179
366
  c_search.add_argument('--vendor', '-v', type=str, help='Generic component search')
@@ -184,127 +371,939 @@ def setup_args() -> None:
184
371
  c_search.set_defaults(func=comp_search)
185
372
 
186
373
  # Component Sub-command: component versions
187
- c_versions = comp_sub.add_parser('versions', aliases=['vs'],
188
- description=f'Get component version details: {__version__}',
189
- help='Search for component versions')
374
+ c_versions = comp_sub.add_parser(
375
+ 'versions',
376
+ aliases=['vs'],
377
+ description=f'Get component version details: {__version__}',
378
+ help='Search for component versions',
379
+ )
190
380
  c_versions.add_argument('--input', '-i', type=str, help='Input file name')
191
381
  c_versions.add_argument('--purl', '-p', type=str, help='Generic component search')
192
382
  c_versions.add_argument('--limit', '-l', type=int, help='Generic component search')
193
383
  c_versions.set_defaults(func=comp_versions)
194
384
 
385
+ # Sub-command: crypto
386
+ p_crypto = subparsers.add_parser(
387
+ 'crypto',
388
+ aliases=['cr'],
389
+ description=f'SCANOSS Crypto commands: {__version__}',
390
+ help='Crypto support commands',
391
+ )
392
+ crypto_sub = p_crypto.add_subparsers(
393
+ title='Crypto Commands',
394
+ dest='subparsercmd',
395
+ description='crypto sub-commands',
396
+ help='crypto sub-commands',
397
+ )
398
+
399
+ # GetAlgorithms and GetAlgorithmsInRange gRPC APIs
400
+ p_crypto_algorithms = crypto_sub.add_parser(
401
+ 'algorithms',
402
+ aliases=['alg'],
403
+ description=f'Show Cryptographic algorithms: {__version__}',
404
+ help='Retrieve cryptographic algorithms for the given components',
405
+ )
406
+ p_crypto_algorithms.add_argument(
407
+ '--with-range',
408
+ action='store_true',
409
+ help='Returns the list of versions in the specified range that contains cryptographic algorithms',
410
+ )
411
+ p_crypto_algorithms.set_defaults(func=crypto_algorithms)
412
+
413
+ # GetEncryptionHints and GetHintsInRange gRPC APIs
414
+ p_crypto_hints = crypto_sub.add_parser(
415
+ 'hints',
416
+ description=f'Show Encryption hints: {__version__}',
417
+ help='Retrieve encryption hints for the given components',
418
+ )
419
+ p_crypto_hints.add_argument(
420
+ '--with-range',
421
+ action='store_true',
422
+ help='Returns the list of versions in the specified range that contains encryption hints',
423
+ )
424
+ p_crypto_hints.set_defaults(func=crypto_hints)
425
+
426
+ p_crypto_versions_in_range = crypto_sub.add_parser(
427
+ 'versions-in-range',
428
+ aliases=['vr'],
429
+ description=f'Show versions in range: {__version__}',
430
+ help="Given a list of PURLS and version ranges, get a list of versions that do/don't contain crypto algorithms",
431
+ )
432
+ p_crypto_versions_in_range.set_defaults(func=crypto_versions_in_range)
433
+
195
434
  # Common purl Component sub-command options
196
- for p in [c_crypto, c_vulns, c_semgrep]:
197
- p.add_argument('--purl', '-p', type=str, nargs="*", help='Package URL - PURL to process.')
435
+ for p in [
436
+ c_vulns,
437
+ c_semgrep,
438
+ c_provenance,
439
+ p_crypto_algorithms,
440
+ p_crypto_hints,
441
+ p_crypto_versions_in_range,
442
+ c_licenses,
443
+ ]:
444
+ p.add_argument('--purl', '-p', type=str, nargs='*', help='Package URL - PURL to process.')
198
445
  p.add_argument('--input', '-i', type=str, help='Input file name')
446
+
199
447
  # Common Component sub-command options
200
- for p in [c_crypto, c_vulns, c_search, c_versions, c_semgrep]:
201
- p.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).')
202
- p.add_argument('--timeout', '-M', type=int, default=600,
203
- help='Timeout (in seconds) for API communication (optional - default 600)')
448
+ for p in [
449
+ c_vulns,
450
+ c_search,
451
+ c_versions,
452
+ c_semgrep,
453
+ c_provenance,
454
+ p_crypto_algorithms,
455
+ p_crypto_hints,
456
+ p_crypto_versions_in_range,
457
+ c_licenses,
458
+ ]:
459
+ p.add_argument(
460
+ '--timeout',
461
+ '-M',
462
+ type=int,
463
+ default=DEFAULT_API_TIMEOUT,
464
+ help='Timeout (in seconds) for API communication (optional - default 600)',
465
+ )
204
466
 
205
467
  # Sub-command: utils
206
- p_util = subparsers.add_parser('utils', aliases=['ut'],
207
- description=f'SCANOSS Utility commands: {__version__}',
208
- help='General utility support commands')
468
+ p_util = subparsers.add_parser(
469
+ 'utils',
470
+ aliases=['ut'],
471
+ description=f'SCANOSS Utility commands: {__version__}',
472
+ help='General utility support commands',
473
+ )
209
474
 
210
- utils_sub = p_util.add_subparsers(title='Utils Commands', dest='subparsercmd', description='utils sub-commands',
211
- help='utils sub-commands')
475
+ utils_sub = p_util.add_subparsers(
476
+ title='Utils Commands', dest='subparsercmd', description='utils sub-commands', help='utils sub-commands'
477
+ )
212
478
 
213
479
  # Utils Sub-command: utils fast
214
- p_f_f = utils_sub.add_parser('fast',
215
- description=f'Is fast winnowing enabled: {__version__}', help='SCANOSS fast winnowing')
480
+ p_f_f = utils_sub.add_parser(
481
+ 'fast', description=f'Is fast winnowing enabled: {__version__}', help='SCANOSS fast winnowing'
482
+ )
216
483
  p_f_f.set_defaults(func=fast)
217
484
 
218
485
  # Utils Sub-command: utils certloc
219
- p_c_loc = utils_sub.add_parser('certloc', aliases=['cl'],
220
- description=f'Show location of Python CA Certs: {__version__}',
221
- help='Display the location of Python CA Certs')
486
+ p_c_loc = utils_sub.add_parser(
487
+ 'certloc',
488
+ aliases=['cl'],
489
+ description=f'Show location of Python CA Certs: {__version__}',
490
+ help='Display the location of Python CA Certs',
491
+ )
222
492
  p_c_loc.set_defaults(func=utils_certloc)
223
493
 
224
494
  # Utils Sub-command: utils cert-download
225
- p_c_dwnld = utils_sub.add_parser('cert-download', aliases=['cdl', 'cert-dl'],
226
- description=f'Download Server SSL Cert: {__version__}',
227
- help='Download the specified server\'s SSL PEM certificate')
495
+ p_c_dwnld = utils_sub.add_parser(
496
+ 'cert-download',
497
+ aliases=['cdl', 'cert-dl'],
498
+ description=f'Download Server SSL Cert: {__version__}',
499
+ help="Download the specified server's SSL PEM certificate",
500
+ )
228
501
  p_c_dwnld.set_defaults(func=utils_cert_download)
229
502
  p_c_dwnld.add_argument('--hostname', '-n', required=True, type=str, help='Server hostname to download cert from.')
230
- p_c_dwnld.add_argument('--port', '-p', required=False, type=int, default=443,
231
- help='Server port number (default: 443).')
232
- p_c_dwnld.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).')
503
+ p_c_dwnld.add_argument(
504
+ '--port', '-p', required=False, type=int, default=443, help='Server port number (default: 443).'
505
+ )
233
506
 
234
507
  # Utils Sub-command: utils pac-proxy
235
- p_p_proxy = utils_sub.add_parser('pac-proxy', aliases=['pac'],
236
- description=f'Determine Proxy from PAC: {__version__}',
237
- help='Use Proxy Auto-Config to determine proxy configuration')
508
+ p_p_proxy = utils_sub.add_parser(
509
+ 'pac-proxy',
510
+ aliases=['pac'],
511
+ description=f'Determine Proxy from PAC: {__version__}',
512
+ help='Use Proxy Auto-Config to determine proxy configuration',
513
+ )
238
514
  p_p_proxy.set_defaults(func=utils_pac_proxy)
239
- p_p_proxy.add_argument('--pac', required=False, type=str, default="auto",
240
- help='Proxy auto configuration. Specify a file, http url or "auto" to try to discover it.'
241
- )
242
- p_p_proxy.add_argument('--url', required=False, type=str, default="https://api.osskb.org",
243
- help='URL to test (default: https://api.osskb.org).')
515
+ p_p_proxy.add_argument(
516
+ '--pac',
517
+ required=False,
518
+ type=str,
519
+ default='auto',
520
+ help='Proxy auto configuration. Specify a file, http url or "auto" to try to discover it.',
521
+ )
522
+ p_p_proxy.add_argument(
523
+ '--url',
524
+ required=False,
525
+ type=str,
526
+ default='https://api.osskb.org',
527
+ help='URL to test (default: https://api.osskb.org).',
528
+ )
529
+
530
+ p_results = subparsers.add_parser(
531
+ 'results',
532
+ aliases=['res'],
533
+ description=f'SCANOSS Results commands: {__version__}',
534
+ help='Process scan results',
535
+ )
536
+ p_results.add_argument(
537
+ 'filepath',
538
+ metavar='FILEPATH',
539
+ type=str,
540
+ nargs='?',
541
+ help='Path to the file containing the results',
542
+ )
543
+ p_results.add_argument(
544
+ '--match-type',
545
+ '-mt',
546
+ help='Filter results by match type (comma-separated, e.g., file,snippet)',
547
+ )
548
+ p_results.add_argument(
549
+ '--status',
550
+ '-s',
551
+ help='Filter results by file status (comma-separated, e.g., pending, identified)',
552
+ )
553
+ p_results.add_argument(
554
+ '--has-pending',
555
+ action='store_true',
556
+ help='Filter results to only include files with pending status',
557
+ )
558
+ p_results.add_argument(
559
+ '--output',
560
+ '-o',
561
+ help='Output result file',
562
+ )
563
+ p_results.add_argument(
564
+ '--format',
565
+ '-f',
566
+ choices=['json', 'plain'],
567
+ help='Output format (default: plain)',
568
+ )
569
+ p_results.set_defaults(func=results)
570
+
571
+ # =========================================================================
572
+ # INSPECT SUBCOMMAND - Analysis and validation of scan results
573
+ # =========================================================================
574
+
575
+ # Main inspect parser - provides tools for analyzing scan results
576
+ p_inspect = subparsers.add_parser(
577
+ 'inspect',
578
+ aliases=['insp', 'ins'],
579
+ description=f'Inspect and analyse scan results: {__version__}',
580
+ help='Inspect and analyse scan results',
581
+ )
582
+
583
+ # Inspect sub-commands parser
584
+ p_inspect_sub = p_inspect.add_subparsers(
585
+ title='Inspect Commands',
586
+ dest='subparsercmd',
587
+ description='Available inspection sub-commands',
588
+ help='Choose an inspection type',
589
+ )
590
+
591
+ # -------------------------------------------------------------------------
592
+ # RAW RESULTS INSPECTION - Analyse raw scan output
593
+ # -------------------------------------------------------------------------
594
+
595
+ # Raw results parser - handles inspection of unprocessed scan results
596
+ p_inspect_raw = p_inspect_sub.add_parser(
597
+ 'raw',
598
+ description='Inspect and analyse SCANOSS raw scan results',
599
+ help='Analyse raw scan results for various compliance issues',
600
+ )
601
+
602
+ # Raw results sub-commands parser
603
+ p_inspect_raw_sub = p_inspect_raw.add_subparsers(
604
+ title='Raw Results Inspection Commands',
605
+ dest='subparser_subcmd',
606
+ description='Tools for analyzing raw scan results',
607
+ help='Choose a raw results analysis type',
608
+ )
609
+
610
+ # Copyleft license inspection - identifies copyleft license violations
611
+ p_inspect_raw_copyleft = p_inspect_raw_sub.add_parser(
612
+ 'copyleft',
613
+ aliases=['cp'],
614
+ description='Identify components with copyleft licenses that may require compliance action',
615
+ help='Find copyleft license violations',
616
+ )
617
+
618
+ # License summary inspection - provides overview of all detected licenses
619
+ p_inspect_raw_license_summary = p_inspect_raw_sub.add_parser(
620
+ 'license-summary',
621
+ aliases=['lic-summary', 'licsum'],
622
+ description='Generate comprehensive summary of all licenses found in scan results',
623
+ help='Generate license summary report',
624
+ )
625
+
626
+ # Component summary inspection - provides overview of all detected components
627
+ p_inspect_raw_component_summary = p_inspect_raw_sub.add_parser(
628
+ 'component-summary',
629
+ aliases=['comp-summary', 'compsum'],
630
+ description='Generate comprehensive summary of all components found in scan results',
631
+ help='Generate component summary report',
632
+ )
633
+
634
+ # Undeclared components inspection - finds components not declared in SBOM
635
+ p_inspect_raw_undeclared = p_inspect_raw_sub.add_parser(
636
+ 'undeclared',
637
+ aliases=['un'],
638
+ description='Identify components present in code but not declared in SBOM files',
639
+ help='Find undeclared components',
640
+ )
641
+ # SBOM format option for undeclared components inspection
642
+ p_inspect_raw_undeclared.add_argument(
643
+ '--sbom-format',
644
+ required=False,
645
+ choices=['legacy', 'settings'],
646
+ default='settings',
647
+ help='SBOM format type for comparison: legacy or settings (default)',
648
+ )
649
+
650
+ # -------------------------------------------------------------------------
651
+ # BACKWARD COMPATIBILITY - Support old inspect command format
652
+ # -------------------------------------------------------------------------
653
+
654
+ # Legacy copyleft inspection - backward compatibility for 'scanoss-py inspect copyleft'
655
+ p_inspect_legacy_copyleft = p_inspect_sub.add_parser(
656
+ 'copyleft',
657
+ aliases=['cp'],
658
+ description='Identify components with copyleft licenses that may require compliance action',
659
+ help='Find copyleft license violations (legacy format)',
660
+ )
661
+
662
+ # Legacy undeclared components inspection - backward compatibility for 'scanoss-py inspect undeclared'
663
+ p_inspect_legacy_undeclared = p_inspect_sub.add_parser(
664
+ 'undeclared',
665
+ aliases=['un'],
666
+ description='Identify components present in code but not declared in SBOM files',
667
+ help='Find undeclared components (legacy format)',
668
+ )
669
+
670
+ # SBOM format option for legacy undeclared components inspection
671
+ p_inspect_legacy_undeclared.add_argument(
672
+ '--sbom-format',
673
+ required=False,
674
+ choices=['legacy', 'settings'],
675
+ default='settings',
676
+ help='SBOM format type for comparison: legacy or settings (default)',
677
+ )
678
+
679
+ # Legacy license summary inspection - backward compatibility for 'scanoss-py inspect license-summary'
680
+ p_inspect_legacy_license_summary = p_inspect_sub.add_parser(
681
+ 'license-summary',
682
+ aliases=['lic-summary', 'licsum'],
683
+ description='Generate comprehensive summary of all licenses found in scan results',
684
+ help='Generate license summary report (legacy format)',
685
+ )
686
+
687
+ # Legacy component summary inspection - backward compatibility for 'scanoss-py inspect component-summary'
688
+ p_inspect_legacy_component_summary = p_inspect_sub.add_parser(
689
+ 'component-summary',
690
+ aliases=['comp-summary', 'compsum'],
691
+ description='Generate comprehensive summary of all components found in scan results',
692
+ help='Generate component summary report (legacy format)',
693
+ )
694
+
695
+ # Applies the same configuration to both legacy and raw versions
696
+ # License filtering options - common to (legacy) copyleft and license summary commands
697
+ for p in [
698
+ p_inspect_raw_copyleft,
699
+ p_inspect_raw_license_summary,
700
+ p_inspect_legacy_copyleft,
701
+ p_inspect_legacy_license_summary,
702
+ ]:
703
+ p.add_argument('--include', help='Additional licenses to include in analysis (comma-separated list)')
704
+ p.add_argument('--exclude', help='Licenses to exclude from analysis (comma-separated list)')
705
+ p.add_argument('--explicit', help='Use only these specific licenses for analysis (comma-separated list)')
706
+
707
+ # License source filtering
708
+ for p in [p_inspect_raw_copyleft, p_inspect_legacy_copyleft]:
709
+ p.add_argument(
710
+ '-ls', '--license-sources',
711
+ action='extend',
712
+ nargs='+',
713
+ choices=VALID_LICENSE_SOURCES,
714
+ help=f'Specify which license sources to check for copyleft violations. Each license object in scan results '
715
+ f'has a source field indicating its origin. Default: {", ".join(DEFAULT_COPYLEFT_LICENSE_SOURCES)}',
716
+ )
717
+
718
+ # Common options for (legacy) copyleft and undeclared component inspection
719
+ for p in [p_inspect_raw_copyleft, p_inspect_raw_undeclared, p_inspect_legacy_copyleft, p_inspect_legacy_undeclared]:
720
+ p.add_argument('-i', '--input', nargs='?', help='Path to scan results file to analyse')
721
+ p.add_argument(
722
+ '-f',
723
+ '--format',
724
+ required=False,
725
+ choices=['json', 'md', 'jira_md'],
726
+ default='json',
727
+ help='Output format: json (default), md (Markdown), or jira_md (JIRA Markdown)',
728
+ )
729
+ p.add_argument('-o', '--output', type=str, help='Save detailed results to specified file')
730
+ p.add_argument('-s', '--status', type=str, help='Save summary status report to Markdown file')
731
+
732
+ # Common options for (legacy) license and component summary commands
733
+ for p in [
734
+ p_inspect_raw_license_summary,
735
+ p_inspect_raw_component_summary,
736
+ p_inspect_legacy_license_summary,
737
+ p_inspect_legacy_component_summary,
738
+ ]:
739
+ p.add_argument('-i', '--input', nargs='?', help='Path to scan results file to analyse')
740
+ p.add_argument('-o', '--output', type=str, help='Save summary report to specified file')
741
+
742
+ # -------------------------------------------------------------------------
743
+ # DEPENDENCY TRACK INSPECTION - Analyse Dependency Track project data
744
+ # -------------------------------------------------------------------------
745
+
746
+ # Dependency Track parser - handles inspection of DT project status and violations
747
+ p_dep_track_sub = p_inspect_sub.add_parser(
748
+ 'dependency-track',
749
+ aliases=['dt'],
750
+ description='Inspect and analyse Dependency Track project status and policy violations',
751
+ help='Analyse Dependency Track projects',
752
+ )
753
+
754
+ # Dependency Track sub-commands parser
755
+ p_inspect_dep_track_sub = p_dep_track_sub.add_subparsers(
756
+ title='Dependency Track Inspection Commands',
757
+ dest='subparser_subcmd',
758
+ description='Tools for analysing Dependency Track project data',
759
+ help='Choose a Dependency Track analysis type',
760
+ )
761
+
762
+ # Project violations inspection - analyses policy violations in DT projects
763
+ p_inspect_dt_project_violation = p_inspect_dep_track_sub.add_parser(
764
+ 'project-violations',
765
+ aliases=['pv'],
766
+ description='Analyse policy violations and compliance issues in Dependency Track projects',
767
+ help='Inspect project policy violations',
768
+ )
769
+ # Dependency Track connection and authentication options
770
+ p_inspect_dt_project_violation.add_argument(
771
+ '--url', required=True, type=str, help='Dependency Track server base URL (e.g., https://dtrack.example.com)'
772
+ )
773
+ p_inspect_dt_project_violation.add_argument(
774
+ '--upload-token',
775
+ '-ut',
776
+ required=False,
777
+ type=str,
778
+ help='Project-specific upload token for accessing DT project data',
779
+ )
780
+ p_inspect_dt_project_violation.add_argument(
781
+ '--project-id', '-pid', required=False, type=str, help='Dependency Track project UUID to inspect'
782
+ )
783
+ p_inspect_dt_project_violation.add_argument(
784
+ '--apikey', '-k', required=True, type=str, help='Dependency Track API key for authentication'
785
+ )
786
+ p_inspect_dt_project_violation.add_argument(
787
+ '--project-name', '-pn', required=False, type=str, help='Dependency Track project name'
788
+ )
789
+ p_inspect_dt_project_violation.add_argument(
790
+ '--project-version', '-pv', required=False, type=str, help='Dependency Track project version'
791
+ )
792
+ p_inspect_dt_project_violation.add_argument(
793
+ '--output', '-o', required=False, type=str, help='Save inspection results to specified file'
794
+ )
795
+ p_inspect_dt_project_violation.add_argument(
796
+ '--status', required=False, type=str, help='Save summary status report to specified file'
797
+ )
798
+ p_inspect_dt_project_violation.add_argument(
799
+ '--format',
800
+ '-f',
801
+ required=False,
802
+ choices=['json', 'md', 'jira_md'],
803
+ default='json',
804
+ help='Output format: json (default), md (Markdown) or jira_md (JIRA Markdown)',
805
+ )
806
+ p_inspect_dt_project_violation.add_argument(
807
+ '--timeout',
808
+ '-M',
809
+ required=False,
810
+ default=300,
811
+ type=float,
812
+ help='Timeout (in seconds) for API communication (optional - default 300 sec)',
813
+ )
814
+
815
+ # ==============================================================================
816
+ # GitLab Integration Parser
817
+ # ==============================================================================
818
+ # Main parser for GitLab-specific inspection commands and report generation
819
+ p_gitlab_sub = p_inspect_sub.add_parser(
820
+ 'gitlab',
821
+ aliases=['glc'],
822
+ description='Generate GitLab-compatible reports from SCANOSS scan results (Markdown summaries)',
823
+ help='Generate GitLab integration reports',
824
+ )
825
+
826
+ # GitLab sub-commands parser
827
+ # Provides access to different GitLab report formats and inspection tools
828
+ p_gitlab_sub_parser = p_gitlab_sub.add_subparsers(
829
+ title='GitLab Report Types',
830
+ dest='subparser_subcmd',
831
+ description='Available GitLab report formats for scan result analysis',
832
+ help='Select the type of GitLab report to generate',
833
+ )
834
+
835
+ # ==============================================================================
836
+ # GitLab Matches Summary Command
837
+ # ==============================================================================
838
+ # Analyzes scan results and generates a GitLab-compatible Markdown summary
839
+ p_gl_inspect_matches = p_gitlab_sub_parser.add_parser(
840
+ 'matches',
841
+ aliases=['ms'],
842
+ description='Generate a Markdown summary report of scan matches for GitLab integration',
843
+ help='Generate Markdown summary report of scan matches',
844
+ )
845
+
846
+ # Input file argument - SCANOSS scan results in JSON format
847
+ p_gl_inspect_matches.add_argument(
848
+ '-i', '--input', required=True, type=str, help='Path to SCANOSS scan results file (JSON format) to analyze'
849
+ )
850
+
851
+ # Line range prefix for GitLab file navigation
852
+ # Enables clickable file references in the generated report that link to specific lines in GitLab
853
+ p_gl_inspect_matches.add_argument(
854
+ '-lpr',
855
+ '--line-range-prefix',
856
+ required=True,
857
+ type=str,
858
+ help='Base URL prefix for GitLab file links with line ranges (e.g., https://gitlab.com/org/project/-/blob/main)',
859
+ )
860
+
861
+ # Output file argument - where to save the generated Markdown report
862
+ p_gl_inspect_matches.add_argument(
863
+ '--output',
864
+ '-o',
865
+ required=False,
866
+ type=str,
867
+ help='Output file path for the generated Markdown report (default: stdout)',
868
+ )
869
+
870
+ # TODO Move to the command call def location
871
+ # RAW results
872
+ p_inspect_raw_undeclared.set_defaults(func=inspect_undeclared)
873
+ p_inspect_raw_copyleft.set_defaults(func=inspect_copyleft)
874
+ p_inspect_raw_license_summary.set_defaults(func=inspect_license_summary)
875
+ p_inspect_raw_component_summary.set_defaults(func=inspect_component_summary)
876
+ # Legacy backward compatibility commands
877
+ p_inspect_legacy_copyleft.set_defaults(func=inspect_copyleft)
878
+ p_inspect_legacy_undeclared.set_defaults(func=inspect_undeclared)
879
+ p_inspect_legacy_license_summary.set_defaults(func=inspect_license_summary)
880
+ p_inspect_legacy_component_summary.set_defaults(func=inspect_component_summary)
881
+ # Dependency Track
882
+ p_inspect_dt_project_violation.set_defaults(func=inspect_dep_track_project_violations)
883
+ # GitLab
884
+ p_gl_inspect_matches.set_defaults(func=inspect_gitlab_matches)
885
+
886
+ # =========================================================================
887
+ # END INSPECT SUBCOMMAND CONFIGURATION
888
+ # =========================================================================
889
+
890
+ # Sub-command: export
891
+ p_export = subparsers.add_parser(
892
+ 'export',
893
+ aliases=['exp'],
894
+ description=f'Export SBOM files to external platforms: {__version__}',
895
+ help='Export SBOM files to external platforms',
896
+ )
897
+
898
+ export_sub = p_export.add_subparsers(
899
+ title='Export Commands',
900
+ dest='subparsercmd',
901
+ description='export sub-commands',
902
+ help='export sub-commands',
903
+ )
904
+
905
+ # Export Sub-command: export dt (Dependency Track)
906
+ e_dt = export_sub.add_parser(
907
+ 'dt',
908
+ aliases=['dependency-track'],
909
+ description='Export SBOM to Dependency Track',
910
+ help='Upload SBOM files to Dependency Track',
911
+ )
912
+ e_dt.add_argument('-i', '--input', type=str, required=True, help='Input SBOM file (CycloneDX JSON format)')
913
+ e_dt.add_argument('--url', type=str, required=True, help='Dependency Track base URL')
914
+ e_dt.add_argument('--apikey', '-k', type=str, required=True, help='Dependency Track API key')
915
+ e_dt.add_argument('--output', '-o', type=str, help='File to save export token and uuid into')
916
+ e_dt.add_argument('--project-id', '-pid', type=str, help='Dependency Track project UUID')
917
+ e_dt.add_argument('--project-name', '-pn', type=str, help='Dependency Track project name')
918
+ e_dt.add_argument('--project-version', '-pv', type=str, help='Dependency Track project version')
919
+ e_dt.set_defaults(func=export_dt)
920
+
921
+ # Sub-command: folder-scan
922
+ p_folder_scan = subparsers.add_parser(
923
+ 'folder-scan',
924
+ aliases=['fs'],
925
+ description=f'Scan the given directory using folder hashing: {__version__}',
926
+ help='Scan the given directory using folder hashing',
927
+ )
928
+ p_folder_scan.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', help='The root directory to scan')
929
+ p_folder_scan.add_argument(
930
+ '--timeout',
931
+ '-M',
932
+ type=int,
933
+ default=600,
934
+ help='Timeout (in seconds) for API communication (optional - default 600)',
935
+ )
936
+ p_folder_scan.add_argument(
937
+ '--format',
938
+ '-f',
939
+ type=str,
940
+ choices=['json', 'cyclonedx'],
941
+ default='json',
942
+ help='Result output format (optional - default: json)',
943
+ )
944
+ p_folder_scan.add_argument(
945
+ '--rank-threshold',
946
+ type=int,
947
+ default=DEFAULT_HFH_RANK_THRESHOLD,
948
+ help='Filter results to only show those with rank value at or below this threshold (e.g., --rank-threshold 3 '
949
+ 'returns results with rank 1, 2, or 3). Lower rank values indicate higher quality matches.',
950
+ )
951
+ p_folder_scan.add_argument(
952
+ '--depth',
953
+ type=int,
954
+ default=DEFAULT_HFH_DEPTH,
955
+ help=f'Defines how deep to scan the root directory (optional - default {DEFAULT_HFH_DEPTH})',
956
+ )
957
+ p_folder_scan.add_argument(
958
+ '--recursive-threshold',
959
+ type=float,
960
+ default=DEFAULT_HFH_RECURSIVE_THRESHOLD,
961
+ help=f'Minimum score threshold to consider a match (optional - default: {DEFAULT_HFH_RECURSIVE_THRESHOLD})',
962
+ )
963
+ p_folder_scan.add_argument(
964
+ '--min-accepted-score',
965
+ type=float,
966
+ default=DEFAULT_HFH_MIN_ACCEPTED_SCORE,
967
+ help=(
968
+ 'Only show results with a score at or above this threshold '
969
+ f'(optional - default: {DEFAULT_HFH_MIN_ACCEPTED_SCORE})'
970
+ ),
971
+ )
972
+ p_folder_scan.set_defaults(func=folder_hashing_scan)
973
+
974
+ # Sub-command: folder-hash
975
+ p_folder_hash = subparsers.add_parser(
976
+ 'folder-hash',
977
+ aliases=['fh'],
978
+ description=f'Produce a folder hash for the given directory: {__version__}',
979
+ help='Produce a folder hash for the given directory',
980
+ )
981
+ p_folder_hash.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', help='A file or folder to scan')
982
+ p_folder_hash.add_argument(
983
+ '--format',
984
+ '-f',
985
+ type=str,
986
+ choices=['json'],
987
+ default='json',
988
+ help='Result output format (optional - default: json)',
989
+ )
990
+ p_folder_hash.add_argument(
991
+ '--depth',
992
+ type=int,
993
+ default=DEFAULT_HFH_DEPTH,
994
+ help=f'Defines how deep to hash the root directory (optional - default {DEFAULT_HFH_DEPTH})',
995
+ )
996
+ p_folder_hash.set_defaults(func=folder_hash)
997
+
998
+ # Sub-command: delta
999
+ p_delta = subparsers.add_parser(
1000
+ 'delta',
1001
+ aliases=['dl'],
1002
+ description=f'SCANOSS Delta commands: {__version__}',
1003
+ help='Delta support commands',
1004
+ )
1005
+
1006
+ delta_sub = p_delta.add_subparsers(
1007
+ title='Delta Commands', dest='subparsercmd', description='Delta sub-commands', help='Delta sub-commands'
1008
+ )
1009
+
1010
+ # Delta Sub-command: copy
1011
+ p_copy = delta_sub.add_parser(
1012
+ 'copy',
1013
+ aliases=['cp'],
1014
+ description=f'Copy file list into delta dir: {__version__}',
1015
+ help='Copy the given list of files into a delta directory',
1016
+ )
1017
+ p_copy.add_argument('--input', '-i', type=str, required=True, help='Input file with diff list')
1018
+ p_copy.add_argument('--folder', '-fd', type=str, help='Delta folder to copy into')
1019
+ p_copy.add_argument('--root', '-rd', type=str, help='Root directory to place delta folder')
1020
+ p_copy.set_defaults(func=delta_copy)
1021
+
1022
+ # Output options
1023
+ for p in [
1024
+ p_scan,
1025
+ p_cs,
1026
+ p_wfp,
1027
+ p_dep,
1028
+ p_fc,
1029
+ p_cnv,
1030
+ c_vulns,
1031
+ c_search,
1032
+ c_versions,
1033
+ c_semgrep,
1034
+ c_provenance,
1035
+ p_c_dwnld,
1036
+ p_folder_scan,
1037
+ p_folder_hash,
1038
+ p_crypto_algorithms,
1039
+ p_crypto_hints,
1040
+ p_crypto_versions_in_range,
1041
+ c_licenses,
1042
+ p_copy,
1043
+ ]:
1044
+ p.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).')
1045
+
1046
+ # Format options
1047
+ for p in [p_scan, p_cs]:
1048
+ choices = ['plain', 'cyclonedx', 'spdxlite', 'csv']
1049
+ if p is p_cs:
1050
+ choices.append('raw')
1051
+
1052
+ p.add_argument(
1053
+ '--format',
1054
+ '-f',
1055
+ type=str,
1056
+ choices=choices,
1057
+ default='plain',
1058
+ help='Result output format (optional - default: plain)',
1059
+ )
1060
+
1061
+ # Scanoss settings options
1062
+ for p in [p_folder_scan, p_scan, p_wfp, p_folder_hash]:
1063
+ p.add_argument(
1064
+ '--settings',
1065
+ '-st',
1066
+ type=str,
1067
+ help='Settings file to use for scanning (optional - default scanoss.json)',
1068
+ )
1069
+ p.add_argument(
1070
+ '--skip-settings-file',
1071
+ '-stf',
1072
+ action='store_true',
1073
+ help='Skip default settings file (scanoss.json) if it exists',
1074
+ )
244
1075
 
245
1076
  # Global Scan command options
246
- for p in [p_scan]:
247
- p.add_argument('--apiurl', type=str,
248
- help='SCANOSS API URL (optional - default: https://api.osskb.org/scan/direct)')
249
- p.add_argument('--ignore-cert-errors', action='store_true', help='Ignore certificate errors')
1077
+ for p in [p_scan, p_cs]:
1078
+ p.add_argument(
1079
+ '--apiurl', type=str, help='SCANOSS API URL (optional - default: https://api.osskb.org/scan/direct)'
1080
+ )
250
1081
 
251
1082
  # Global Scan/Fingerprint filter options
252
1083
  for p in [p_scan, p_wfp]:
253
1084
  p.add_argument('--obfuscate', action='store_true', help='Obfuscate fingerprints')
254
- p.add_argument('--all-extensions', action='store_true', help='Fingerprint all file extensions')
255
- p.add_argument('--all-folders', action='store_true', help='Fingerprint all folders')
256
- p.add_argument('--all-hidden', action='store_true', help='Fingerprint all hidden files/folders')
1085
+ p.add_argument('--all-extensions', action='store_true', help='Fingerprint all file extensions/types...')
1086
+ p.add_argument('--all-folders', action='store_true', help='Fingerprint all folders...')
1087
+ p.add_argument('--all-hidden', action='store_true', help='Fingerprint all hidden files/folders...')
257
1088
  p.add_argument('--hpsm', '-H', action='store_true', help='Use High Precision Snippet Matching algorithm.')
258
1089
  p.add_argument('--skip-snippets', '-S', action='store_true', help='Skip the generation of snippets')
259
1090
  p.add_argument('--skip-extension', '-E', type=str, action='append', help='File Extension to skip.')
260
1091
  p.add_argument('--skip-folder', '-O', type=str, action='append', help='Folder to skip.')
261
- p.add_argument('--skip-size', '-Z', type=int, default=0,
262
- help='Minimum file size to consider for fingerprinting (optional - default 0 bytes [unlimited])')
1092
+ p.add_argument(
1093
+ '--skip-size',
1094
+ '-Z',
1095
+ type=int,
1096
+ default=0,
1097
+ help='Minimum file size to consider for fingerprinting (optional - default 0 bytes [unlimited])',
1098
+ )
263
1099
  p.add_argument('--skip-md5', '-5', type=str, action='append', help='Skip files matching MD5.')
264
1100
  p.add_argument('--strip-hpsm', '-G', type=str, action='append', help='Strip HPSM string from WFP.')
265
1101
  p.add_argument('--strip-snippet', '-N', type=str, action='append', help='Strip Snippet ID string from WFP.')
1102
+ p.add_argument(
1103
+ '--skip-headers',
1104
+ '-skh',
1105
+ action='store_true',
1106
+ help='Skip license headers, comments and imports at the beginning of files.',
1107
+ )
1108
+ p.add_argument(
1109
+ '--skip-headers-limit',
1110
+ '-shl',
1111
+ type=int,
1112
+ default=0,
1113
+ help='Maximum number of lines to skip when filtering headers (default: 0 = no limit).',
1114
+ )
266
1115
 
267
1116
  # Global Scan/GRPC options
268
- for p in [p_scan, c_crypto, c_vulns, c_search, c_versions, c_semgrep]:
269
- p.add_argument('--key', '-k', type=str,
270
- help='SCANOSS API Key token (optional - not required for default OSSKB URL)')
271
- p.add_argument('--proxy', type=str, help='Proxy URL to use for connections (optional). '
272
- 'Can also use the environment variable "HTTPS_PROXY=<ip>:<port>" '
273
- 'and "grcp_proxy=<ip>:<port>" for gRPC')
274
- p.add_argument('--pac', type=str, help='Proxy auto configuration (optional). '
275
- 'Specify a file, http url or "auto" to try to discover it.')
276
- p.add_argument('--ca-cert', type=str, help='Alternative certificate PEM file (optional). '
277
- 'Can also use the environment variable '
278
- '"REQUESTS_CA_BUNDLE=/path/to/cacert.pem" and '
279
- '"GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=/path/to/cacert.pem" for gRPC')
1117
+ for p in [
1118
+ p_scan,
1119
+ c_vulns,
1120
+ c_search,
1121
+ c_versions,
1122
+ c_semgrep,
1123
+ c_provenance,
1124
+ p_folder_scan,
1125
+ p_cs,
1126
+ p_crypto_algorithms,
1127
+ p_crypto_hints,
1128
+ p_crypto_versions_in_range,
1129
+ c_licenses,
1130
+ ]:
1131
+ p.add_argument(
1132
+ '--key', '-k', type=str, help='SCANOSS API Key token (optional - not required for default OSSKB URL)'
1133
+ )
1134
+ p.add_argument(
1135
+ '--proxy',
1136
+ type=str,
1137
+ help='Proxy URL to use for connections (optional). '
1138
+ 'Can also use the environment variable "HTTPS_PROXY=<ip>:<port>" '
1139
+ 'and "grcp_proxy=<ip>:<port>" for gRPC',
1140
+ )
1141
+ p.add_argument(
1142
+ '--pac',
1143
+ type=str,
1144
+ help='Proxy auto configuration (optional). Specify a file, http url or "auto" to try to discover it.',
1145
+ )
1146
+ p.add_argument(
1147
+ '--ca-cert',
1148
+ type=str,
1149
+ help='Alternative certificate PEM file (optional). '
1150
+ 'Can also use the environment variable '
1151
+ '"REQUESTS_CA_BUNDLE=/path/to/cacert.pem" and '
1152
+ '"GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=/path/to/cacert.pem" for gRPC',
1153
+ )
280
1154
 
281
1155
  # Global GRPC options
282
- for p in [p_scan, c_crypto, c_vulns, c_search, c_versions, c_semgrep]:
283
- p.add_argument('--api2url', type=str,
284
- help='SCANOSS gRPC API 2.0 URL (optional - default: https://api.osskb.org)')
285
- p.add_argument('--grpc-proxy', type=str, help='GRPC Proxy URL to use for connections (optional). '
286
- 'Can also use the environment variable "grcp_proxy=<ip>:<port>"')
1156
+ for p in [
1157
+ p_scan,
1158
+ c_vulns,
1159
+ c_search,
1160
+ c_versions,
1161
+ c_semgrep,
1162
+ c_provenance,
1163
+ p_folder_scan,
1164
+ p_cs,
1165
+ p_crypto_algorithms,
1166
+ p_crypto_hints,
1167
+ p_crypto_versions_in_range,
1168
+ c_licenses,
1169
+ ]:
1170
+ p.add_argument(
1171
+ '--api2url', type=str, help='SCANOSS gRPC API 2.0 URL (optional - default: https://api.osskb.org)'
1172
+ )
1173
+ p.add_argument(
1174
+ '--grpc-proxy',
1175
+ type=str,
1176
+ help='GRPC Proxy URL to use for connections (optional). '
1177
+ 'Can also use the environment variable "grcp_proxy=<ip>:<port>"',
1178
+ )
1179
+ p.add_argument(
1180
+ '--header',
1181
+ '-hdr',
1182
+ action='append', # This allows multiple -H flags
1183
+ type=str,
1184
+ help='Headers to be sent on request (e.g., -hdr "Name: Value") - can be used multiple times',
1185
+ )
1186
+ p.add_argument('--ignore-cert-errors', action='store_true', help='Ignore certificate errors')
1187
+
1188
+ # Syft options
1189
+ for p in [p_cs, p_dep]:
1190
+ p.add_argument(
1191
+ '--syft-command',
1192
+ type=str,
1193
+ help='Syft command and path if required (optional - default syft).',
1194
+ default=DEFAULT_SYFT_COMMAND,
1195
+ )
1196
+ p.add_argument(
1197
+ '--syft-timeout',
1198
+ type=int,
1199
+ default=DEFAULT_SYFT_TIMEOUT,
1200
+ help='Timeout (in seconds) for syft to complete (optional - default 600)',
1201
+ )
1202
+
1203
+ # gRPC support options
1204
+ for p in [
1205
+ c_vulns,
1206
+ p_scan,
1207
+ p_cs,
1208
+ p_crypto_algorithms,
1209
+ p_crypto_hints,
1210
+ p_crypto_versions_in_range,
1211
+ c_semgrep,
1212
+ c_provenance,
1213
+ c_search,
1214
+ c_versions,
1215
+ c_licenses,
1216
+ p_folder_scan,
1217
+ ]:
1218
+ p.add_argument('--grpc', action='store_true', default=True, help='Use gRPC (default)')
1219
+ p.add_argument('--rest', action='store_true', dest='rest', help='Use REST instead of gRPC')
287
1220
 
288
1221
  # Help/Trace command options
289
- for p in [p_scan, p_wfp, p_dep, p_fc, p_cnv, p_c_loc, p_c_dwnld, p_p_proxy, c_crypto, c_vulns, c_search,
290
- c_versions, c_semgrep]:
291
- p.add_argument('--debug', '-d', action='store_true', help='Enable debug messages')
1222
+ for p in [
1223
+ p_scan,
1224
+ p_wfp,
1225
+ p_dep,
1226
+ p_fc,
1227
+ p_cnv,
1228
+ p_c_loc,
1229
+ p_c_dwnld,
1230
+ p_p_proxy,
1231
+ c_vulns,
1232
+ c_search,
1233
+ c_versions,
1234
+ c_semgrep,
1235
+ p_results,
1236
+ p_inspect_raw_undeclared,
1237
+ p_inspect_raw_copyleft,
1238
+ p_inspect_raw_license_summary,
1239
+ p_inspect_raw_component_summary,
1240
+ p_inspect_legacy_copyleft,
1241
+ p_inspect_legacy_undeclared,
1242
+ p_inspect_legacy_license_summary,
1243
+ p_inspect_legacy_component_summary,
1244
+ p_inspect_dt_project_violation,
1245
+ p_gl_inspect_matches,
1246
+ c_provenance,
1247
+ p_folder_scan,
1248
+ p_folder_hash,
1249
+ p_cs,
1250
+ p_crypto_algorithms,
1251
+ p_crypto_hints,
1252
+ p_crypto_versions_in_range,
1253
+ c_licenses,
1254
+ e_dt,
1255
+ p_copy,
1256
+ ]:
1257
+ p.add_argument(
1258
+ '--debug',
1259
+ '-d',
1260
+ action='store_true',
1261
+ default=os.environ.get('SCANOSS_DEBUG', '').lower() == 'true',
1262
+ help='Enable debug messages (can also be set via environment variable SCANOSS_DEBUG)',
1263
+ )
292
1264
  p.add_argument('--trace', '-t', action='store_true', help='Enable trace messages, including API posts')
293
1265
  p.add_argument('--quiet', '-q', action='store_true', help='Enable quiet mode')
294
1266
 
295
1267
  args = parser.parse_args()
1268
+
1269
+ # TODO: Remove this hack once we go back to using REST as default
1270
+ # Handle --rest overriding --grpc default
1271
+ if hasattr(args, 'rest') and args.rest:
1272
+ args.grpc = False
1273
+
296
1274
  if args.version:
297
1275
  ver(parser, args)
298
- exit(0)
1276
+ sys.exit(0)
299
1277
  if not args.subparser:
300
1278
  parser.print_help() # No sub command subcommand, print general help
301
- exit(1)
302
- else:
303
- if (args.subparser == 'utils' or args.subparser == 'ut' or
304
- args.subparser == 'component' or args.subparser == 'comp') \
305
- and not args.subparsercmd:
306
- parser.parse_args([args.subparser, '--help']) # Force utils helps to be displayed
307
- exit(1)
1279
+ sys.exit(1)
1280
+ elif (
1281
+ args.subparser
1282
+ in (
1283
+ 'utils',
1284
+ 'ut',
1285
+ 'component',
1286
+ 'comp',
1287
+ 'inspect',
1288
+ 'insp',
1289
+ 'ins',
1290
+ 'crypto',
1291
+ 'cr',
1292
+ 'export',
1293
+ 'exp',
1294
+ 'delta',
1295
+ 'dl',
1296
+ )
1297
+ ) and not args.subparsercmd:
1298
+ parser.parse_args([args.subparser, '--help']) # Force utils helps to be displayed
1299
+ sys.exit(1)
1300
+ elif (
1301
+ (args.subparser in 'inspect')
1302
+ and (args.subparsercmd in ('raw', 'dt', 'glc', 'gitlab'))
1303
+ and (args.subparser_subcmd is None)
1304
+ ):
1305
+ parser.parse_args([args.subparser, args.subparsercmd, '--help']) # Force utils helps to be displayed
1306
+ sys.exit(1)
308
1307
  args.func(parser, args) # Execute the function associated with the sub-command
309
1308
 
310
1309
 
@@ -337,23 +1336,25 @@ def file_count(parser, args):
337
1336
  if not args.scan_dir:
338
1337
  print_stderr('Please specify a folder')
339
1338
  parser.parse_args([args.subparser, '-h'])
340
- exit(1)
341
- scan_output: str = None
1339
+ sys.exit(1)
342
1340
  if args.output:
343
- scan_output = args.output
344
- open(scan_output, 'w').close()
345
-
346
- counter = FileCount(debug=args.debug, quiet=args.quiet, trace=args.trace, scan_output=scan_output,
347
- hidden_files_folders=args.all_hidden
348
- )
1341
+ initialise_empty_file(args.output)
1342
+
1343
+ counter = FileCount(
1344
+ debug=args.debug,
1345
+ quiet=args.quiet,
1346
+ trace=args.trace,
1347
+ scan_output=args.output,
1348
+ hidden_files_folders=args.all_hidden,
1349
+ )
349
1350
  if not os.path.exists(args.scan_dir):
350
1351
  print_stderr(f'Error: Folder specified does not exist: {args.scan_dir}.')
351
- exit(1)
1352
+ sys.exit(1)
352
1353
  if os.path.isdir(args.scan_dir):
353
1354
  counter.count_files(args.scan_dir)
354
1355
  else:
355
1356
  print_stderr(f'Error: Path specified is not a folder: {args.scan_dir}.')
356
- exit(1)
1357
+ sys.exit(1)
357
1358
 
358
1359
 
359
1360
  def wfp(parser, args):
@@ -369,38 +1370,60 @@ def wfp(parser, args):
369
1370
  if not args.scan_dir and not args.stdin:
370
1371
  print_stderr('Please specify a file/folder or STDIN (--stdin)')
371
1372
  parser.parse_args([args.subparser, '-h'])
372
- exit(1)
1373
+ sys.exit(1)
373
1374
  if args.strip_hpsm and not args.hpsm and not args.quiet:
374
- print_stderr(f'Warning: --strip-hpsm option supplied without enabling HPSM (--hpsm). Ignoring.')
375
- scan_output: str = None
1375
+ print_stderr('Warning: --strip-hpsm option supplied without enabling HPSM (--hpsm). Ignoring.')
376
1376
  if args.output:
377
- scan_output = args.output
378
- open(scan_output, 'w').close()
1377
+ initialise_empty_file(args.output)
1378
+
1379
+ # Load scan settings
1380
+ scan_settings = None
1381
+ if not args.skip_settings_file:
1382
+ scan_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet)
1383
+ try:
1384
+ scan_settings.load_json_file(args.settings, args.scan_dir)
1385
+ except ScanossSettingsError as e:
1386
+ print_stderr(f'Error: {e}')
1387
+ sys.exit(1)
379
1388
 
380
1389
  scan_options = 0 if args.skip_snippets else ScanType.SCAN_SNIPPETS.value # Skip snippet generation or not
381
- scanner = Scanner(debug=args.debug, trace=args.trace, quiet=args.quiet, obfuscate=args.obfuscate,
382
- scan_options=scan_options, all_extensions=args.all_extensions,
383
- all_folders=args.all_folders, hidden_files_folders=args.all_hidden, hpsm=args.hpsm,
384
- skip_size=args.skip_size, skip_extensions=args.skip_extension, skip_folders=args.skip_folder,
385
- skip_md5_ids=args.skip_md5, strip_hpsm_ids=args.strip_hpsm, strip_snippet_ids=args.strip_snippet
386
- )
1390
+ scanner = Scanner(
1391
+ debug=args.debug,
1392
+ trace=args.trace,
1393
+ quiet=args.quiet,
1394
+ obfuscate=args.obfuscate,
1395
+ scan_options=scan_options,
1396
+ all_extensions=args.all_extensions,
1397
+ all_folders=args.all_folders,
1398
+ hidden_files_folders=args.all_hidden,
1399
+ hpsm=args.hpsm,
1400
+ skip_size=args.skip_size,
1401
+ skip_extensions=args.skip_extension,
1402
+ skip_folders=args.skip_folder,
1403
+ skip_md5_ids=args.skip_md5,
1404
+ strip_hpsm_ids=args.strip_hpsm,
1405
+ strip_snippet_ids=args.strip_snippet,
1406
+ scan_settings=scan_settings,
1407
+ skip_headers=args.skip_headers,
1408
+ skip_headers_limit=args.skip_headers_limit,
1409
+ )
387
1410
  if args.stdin:
388
1411
  contents = sys.stdin.buffer.read()
389
- scanner.wfp_contents(args.stdin, contents, scan_output)
1412
+ scanner.wfp_contents(args.stdin, contents, args.output)
390
1413
  elif args.scan_dir:
391
1414
  if not os.path.exists(args.scan_dir):
392
1415
  print_stderr(f'Error: File or folder specified does not exist: {args.scan_dir}.')
393
- exit(1)
1416
+ sys.exit(1)
394
1417
  if os.path.isdir(args.scan_dir):
395
- scanner.wfp_folder(args.scan_dir, scan_output)
1418
+ scanner.wfp_folder(args.scan_dir, args.output)
396
1419
  elif os.path.isfile(args.scan_dir):
397
- scanner.wfp_file(args.scan_dir, scan_output)
1420
+ scanner.wfp_file(args.scan_dir, args.output)
398
1421
  else:
399
1422
  print_stderr(f'Error: Path specified is neither a file or a folder: {args.scan_dir}.')
400
- exit(1)
1423
+ sys.exit(1)
401
1424
  else:
402
1425
  print_stderr('No action found to process')
403
- exit(1)
1426
+ sys.exit(1)
404
1427
 
405
1428
 
406
1429
  def get_scan_options(args):
@@ -424,18 +1447,18 @@ def get_scan_options(args):
424
1447
 
425
1448
  if args.debug:
426
1449
  if ScanType.SCAN_FILES.value & scan_options:
427
- print_stderr(f'Scan Files')
1450
+ print_stderr('Scan Files')
428
1451
  if ScanType.SCAN_SNIPPETS.value & scan_options:
429
- print_stderr(f'Scan Snippets')
1452
+ print_stderr('Scan Snippets')
430
1453
  if ScanType.SCAN_DEPENDENCIES.value & scan_options:
431
- print_stderr(f'Scan Dependencies')
1454
+ print_stderr('Scan Dependencies')
432
1455
  if scan_options <= 0:
433
1456
  print_stderr(f'Error: No valid scan options configured: {scan_options}')
434
- exit(1)
1457
+ sys.exit(1)
435
1458
  return scan_options
436
1459
 
437
1460
 
438
- def scan(parser, args):
1461
+ def scan(parser, args): # noqa: PLR0912, PLR0915
439
1462
  """
440
1463
  Run the "scan" sub-command
441
1464
  Parameters
@@ -445,66 +1468,77 @@ def scan(parser, args):
445
1468
  args: Namespace
446
1469
  Parsed arguments
447
1470
  """
448
- if not args.scan_dir and not args.wfp and not args.stdin and not args.dep:
449
- print_stderr('Please specify a file/folder, fingerprint (--wfp), dependency (--dep), or STDIN (--stdin)')
1471
+ if not args.scan_dir and not args.wfp and not args.stdin and not args.dep and not args.files:
1472
+ print_stderr(
1473
+ 'Please specify a file/folder, files (--files), fingerprint (--wfp), dependency (--dep), or STDIN (--stdin)'
1474
+ )
450
1475
  parser.parse_args([args.subparser, '-h'])
451
- exit(1)
1476
+ sys.exit(1)
1477
+ if args.no_wfp_output:
1478
+ print_stderr('Warning: --no-wfp-output is deprecated and has no effect. It will be removed in a future version')
452
1479
  if args.pac and args.proxy:
453
1480
  print_stderr('Please specify one of --proxy or --pac, not both')
454
1481
  parser.parse_args([args.subparser, '-h'])
455
- exit(1)
456
- scan_type: str = None
457
- sbom_path: str = None
458
- if args.identify:
459
- sbom_path = args.identify
460
- scan_type = 'identify'
461
- if not os.path.exists(sbom_path) or not os.path.isfile(sbom_path):
462
- print_stderr(f'Specified --identify file does not exist or is not a file: {sbom_path}')
463
- exit(1)
464
- if not Scanner.valid_json_file(sbom_path): # Make sure it's a valid JSON file
465
- exit(1)
466
- if args.ignore:
467
- print_stderr(f'Warning: Specified --identify and --ignore options. Skipping ignore.')
468
- elif args.ignore:
469
- sbom_path = args.ignore
470
- scan_type = 'blacklist'
471
- if not os.path.exists(sbom_path) or not os.path.isfile(sbom_path):
472
- print_stderr(f'Specified --ignore file does not exist or is not a file: {sbom_path}')
473
- exit(1)
474
- if not Scanner.valid_json_file(sbom_path): # Make sure it's a valid JSON file
475
- exit(1)
1482
+ sys.exit(1)
1483
+ if args.identify and args.settings:
1484
+ print_stderr('ERROR: Cannot specify both --identify and --settings options.')
1485
+ sys.exit(1)
1486
+ if args.settings and args.skip_settings_file:
1487
+ print_stderr('ERROR: Cannot specify both --settings and --skip-file-settings options.')
1488
+ sys.exit(1)
1489
+ # Figure out which settings (if any) to load before processing
1490
+ scan_settings = None
1491
+ if not args.skip_settings_file:
1492
+ scan_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet)
1493
+ try:
1494
+ if args.identify:
1495
+ scan_settings.load_json_file(args.identify, args.scan_dir).set_file_type('legacy').set_scan_type(
1496
+ 'identify'
1497
+ )
1498
+ elif args.ignore:
1499
+ scan_settings.load_json_file(args.ignore, args.scan_dir).set_file_type('legacy').set_scan_type(
1500
+ 'blacklist'
1501
+ )
1502
+ else:
1503
+ scan_settings.load_json_file(args.settings, args.scan_dir).set_file_type('new')
1504
+
1505
+ except ScanossSettingsError as e:
1506
+ print_stderr(f'Error: {e}')
1507
+ sys.exit(1)
476
1508
  if args.dep:
477
1509
  if not os.path.exists(args.dep) or not os.path.isfile(args.dep):
478
1510
  print_stderr(f'Specified --dep file does not exist or is not a file: {args.dep}')
479
- exit(1)
480
- if not Scanner.valid_json_file(args.dep): # Make sure it's a valid JSON file
481
- exit(1)
1511
+ sys.exit(1)
1512
+ result = validate_json_file(args.dep)
1513
+ if not result.is_valid:
1514
+ print_stderr(f'Error: Dependency file is not valid: {result.error}')
1515
+ sys.exit(1)
482
1516
  if args.strip_hpsm and not args.hpsm and not args.quiet:
483
- print_stderr(f'Warning: --strip-hpsm option supplied without enabling HPSM (--hpsm). Ignoring.')
1517
+ print_stderr('Warning: --strip-hpsm option supplied without enabling HPSM (--hpsm). Ignoring.')
484
1518
 
485
- scan_output: str = None
486
1519
  if args.output:
487
- scan_output = args.output
488
- open(scan_output, 'w').close()
1520
+ initialise_empty_file(args.output)
489
1521
  output_format = args.format if args.format else 'plain'
490
1522
  flags = args.flags if args.flags else None
491
1523
  if args.debug and not args.quiet:
1524
+ if args.skip_settings_file:
1525
+ print_stderr('Skipping Settings file...')
492
1526
  if args.all_extensions:
493
- print_stderr("Scanning all file extensions/types...")
1527
+ print_stderr('Scanning all file extensions/types...')
494
1528
  if args.all_folders:
495
- print_stderr("Scanning all folders...")
1529
+ print_stderr('Scanning all folders...')
496
1530
  if args.all_hidden:
497
- print_stderr("Scanning all hidden files/folders...")
1531
+ print_stderr('Scanning all hidden files/folders...')
498
1532
  if args.skip_snippets:
499
- print_stderr("Skipping snippets...")
500
- if args.post_size != 32:
1533
+ print_stderr('Skipping snippets...')
1534
+ if args.post_size != DEFAULT_POST_SIZE:
501
1535
  print_stderr(f'Changing scanning POST size to: {args.post_size}k...')
502
- if args.timeout != 180:
1536
+ if args.timeout != DEFAULT_TIMEOUT:
503
1537
  print_stderr(f'Changing scanning POST timeout to: {args.timeout}...')
504
- if args.retry != 5:
1538
+ if args.retry != DEFAULT_RETRY:
505
1539
  print_stderr(f'Changing scanning POST retry to: {args.retry}...')
506
1540
  if args.obfuscate:
507
- print_stderr("Obfuscating file fingerprints...")
1541
+ print_stderr('Obfuscating file fingerprints...')
508
1542
  if args.proxy:
509
1543
  print_stderr(f'Using Proxy {args.proxy}...')
510
1544
  if args.grpc_proxy:
@@ -514,70 +1548,116 @@ def scan(parser, args):
514
1548
  if args.ca_cert:
515
1549
  print_stderr(f'Using Certificate {args.ca_cert}...')
516
1550
  if args.hpsm:
517
- print_stderr("Setting HPSM mode...")
1551
+ print_stderr('Setting HPSM mode...')
518
1552
  if flags:
519
1553
  print_stderr(f'Using flags {flags}...')
520
1554
  elif not args.quiet:
521
- if args.timeout < 5:
1555
+ if args.timeout < MIN_TIMEOUT:
522
1556
  print_stderr(f'POST timeout (--timeout) too small: {args.timeout}. Reverting to default.')
523
1557
  if args.retry < 0:
524
1558
  print_stderr(f'POST retry (--retry) too small: {args.retry}. Reverting to default.')
525
1559
 
526
- if not os.access(os.getcwd(), os.W_OK): # Make sure the current directory is writable. If not disable saving WFP
527
- print_stderr(f'Warning: Current directory is not writable: {os.getcwd()}')
528
- args.no_wfp_output = True
529
1560
  if args.ca_cert and not os.path.exists(args.ca_cert):
530
1561
  print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.')
531
- exit(1)
1562
+ sys.exit(1)
532
1563
  pac_file = get_pac_file(args.pac)
533
- scan_options = get_scan_options(args) # Figure out what scanning options we have
534
-
535
- scanner = Scanner(debug=args.debug, trace=args.trace, quiet=args.quiet, api_key=args.key, url=args.apiurl,
536
- sbom_path=sbom_path, scan_type=scan_type, scan_output=scan_output, output_format=output_format,
537
- flags=flags, nb_threads=args.threads, post_size=args.post_size,
538
- timeout=args.timeout, no_wfp_file=args.no_wfp_output, all_extensions=args.all_extensions,
539
- all_folders=args.all_folders, hidden_files_folders=args.all_hidden,
540
- scan_options=scan_options, sc_timeout=args.sc_timeout, sc_command=args.sc_command,
541
- grpc_url=args.api2url, obfuscate=args.obfuscate,
542
- ignore_cert_errors=args.ignore_cert_errors, proxy=args.proxy, grpc_proxy=args.grpc_proxy,
543
- pac=pac_file, ca_cert=args.ca_cert, retry=args.retry, hpsm=args.hpsm,
544
- skip_size=args.skip_size, skip_extensions=args.skip_extension, skip_folders=args.skip_folder,
545
- skip_md5_ids=args.skip_md5, strip_hpsm_ids=args.strip_hpsm, strip_snippet_ids=args.strip_snippet
546
- )
1564
+ scan_options = get_scan_options(args) # Figure out what scanning options we have
1565
+
1566
+ scanner = Scanner(
1567
+ debug=args.debug,
1568
+ trace=args.trace,
1569
+ quiet=args.quiet,
1570
+ api_key=args.key,
1571
+ url=args.apiurl,
1572
+ scan_output=args.output,
1573
+ output_format=output_format,
1574
+ flags=flags,
1575
+ nb_threads=args.threads,
1576
+ post_size=args.post_size,
1577
+ timeout=args.timeout,
1578
+ all_extensions=args.all_extensions,
1579
+ all_folders=args.all_folders,
1580
+ hidden_files_folders=args.all_hidden,
1581
+ scan_options=scan_options,
1582
+ sc_timeout=args.sc_timeout,
1583
+ sc_command=args.sc_command,
1584
+ grpc_url=args.api2url,
1585
+ obfuscate=args.obfuscate,
1586
+ ignore_cert_errors=args.ignore_cert_errors,
1587
+ proxy=args.proxy,
1588
+ grpc_proxy=args.grpc_proxy,
1589
+ pac=pac_file,
1590
+ ca_cert=args.ca_cert,
1591
+ retry=args.retry,
1592
+ hpsm=args.hpsm,
1593
+ skip_size=args.skip_size,
1594
+ skip_extensions=args.skip_extension,
1595
+ skip_folders=args.skip_folder,
1596
+ skip_md5_ids=args.skip_md5,
1597
+ strip_hpsm_ids=args.strip_hpsm,
1598
+ strip_snippet_ids=args.strip_snippet,
1599
+ scan_settings=scan_settings,
1600
+ req_headers=process_req_headers(args.header),
1601
+ use_grpc=args.grpc,
1602
+ skip_headers=args.skip_headers,
1603
+ skip_headers_limit=args.skip_headers_limit,
1604
+ )
547
1605
  if args.wfp:
548
1606
  if not scanner.is_file_or_snippet_scan():
549
1607
  print_stderr(f'Error: Cannot specify WFP scanning if file/snippet options are disabled ({scan_options})')
550
- exit(1)
1608
+ sys.exit(1)
551
1609
  if scanner.is_dependency_scan() and not args.dep:
552
- print_stderr(f'Error: Cannot specify WFP & Dependency scanning without a dependency file (--dep)')
553
- exit(1)
1610
+ print_stderr('Error: Cannot specify WFP & Dependency scanning without a dependency file (--dep)')
1611
+ sys.exit(1)
554
1612
  scanner.scan_wfp_with_options(args.wfp, args.dep)
555
1613
  elif args.stdin:
556
1614
  contents = sys.stdin.buffer.read()
557
1615
  if not scanner.scan_contents(args.stdin, contents):
558
- exit(1)
1616
+ sys.exit(1)
1617
+ elif args.files:
1618
+ if not scanner.scan_files_with_options(args.files, args.dep, scanner.winnowing.file_map):
1619
+ sys.exit(1)
559
1620
  elif args.scan_dir:
560
1621
  if not os.path.exists(args.scan_dir):
561
1622
  print_stderr(f'Error: File or folder specified does not exist: {args.scan_dir}.')
562
- exit(1)
1623
+ sys.exit(1)
563
1624
  if os.path.isdir(args.scan_dir):
564
- if not scanner.scan_folder_with_options(args.scan_dir, args.dep, scanner.winnowing.file_map):
565
- exit(1)
1625
+ if not scanner.scan_folder_with_options(
1626
+ args.scan_dir,
1627
+ args.dep,
1628
+ scanner.winnowing.file_map,
1629
+ args.dep_scope,
1630
+ args.dep_scope_inc,
1631
+ args.dep_scope_exc,
1632
+ ):
1633
+ sys.exit(1)
566
1634
  elif os.path.isfile(args.scan_dir):
567
- if not scanner.scan_file_with_options(args.scan_dir, args.dep, scanner.winnowing.file_map):
568
- exit(1)
1635
+ if not scanner.scan_file_with_options(
1636
+ args.scan_dir,
1637
+ args.dep,
1638
+ scanner.winnowing.file_map,
1639
+ args.dep_scope,
1640
+ args.dep_scope_inc,
1641
+ args.dep_scope_exc,
1642
+ ):
1643
+ sys.exit(1)
569
1644
  else:
570
1645
  print_stderr(f'Error: Path specified is neither a file or a folder: {args.scan_dir}.')
571
- exit(1)
1646
+ sys.exit(1)
572
1647
  elif args.dep:
573
1648
  if not args.dependencies_only:
574
- print_stderr(f'Error: No file or folder specified to scan. Please add --dependencies-only to decorate dependency file only.')
575
- exit(1)
576
- if not scanner.scan_folder_with_options(".", args.dep, scanner.winnowing.file_map):
577
- exit(1)
1649
+ print_stderr(
1650
+ 'Error: No file or folder specified to scan.'
1651
+ ' Please add --dependencies-only to decorate dependency file only.'
1652
+ )
1653
+ sys.exit(1)
1654
+ if not scanner.scan_folder_with_options(
1655
+ '.', args.dep, scanner.winnowing.file_map, args.dep_scope, args.dep_scope_inc, args.dep_scope_exc
1656
+ ):
1657
+ sys.exit(1)
578
1658
  else:
579
1659
  print_stderr('No action found to process')
580
- exit(1)
1660
+ sys.exit(1)
581
1661
 
582
1662
 
583
1663
  def dependency(parser, args):
@@ -590,23 +1670,28 @@ def dependency(parser, args):
590
1670
  args: Namespace
591
1671
  Parsed arguments
592
1672
  """
593
- if not args.scan_dir:
594
- print_stderr('Please specify a file/folder')
1673
+ if not args.scan_loc and not args.container:
1674
+ print_stderr('Please specify a file/folder or container image')
595
1675
  parser.parse_args([args.subparser, '-h'])
596
- exit(1)
597
- if not os.path.exists(args.scan_dir):
598
- print_stderr(f'Error: File or folder specified does not exist: {args.scan_dir}.')
599
- exit(1)
600
- scan_output: str = None
1676
+ sys.exit(1)
1677
+
1678
+ # Workaround to return syft scan results converted to our dependency output format
1679
+ if args.container:
1680
+ args.scan_loc = args.container
1681
+ return container_scan(parser, args, only_interim_results=True)
1682
+
1683
+ if not os.path.exists(args.scan_loc):
1684
+ print_stderr(f'Error: File or folder specified does not exist: {args.scan_loc}.')
1685
+ sys.exit(1)
601
1686
  if args.output:
602
- scan_output = args.output
603
- open(scan_output, 'w').close()
1687
+ initialise_empty_file(args.output)
604
1688
 
605
- sc_deps = ScancodeDeps(debug=args.debug, quiet=args.quiet, trace=args.trace, sc_command=args.sc_command,
606
- timeout=args.sc_timeout
607
- )
608
- if not sc_deps.get_dependencies(what_to_scan=args.scan_dir, result_output=scan_output):
609
- exit(1)
1689
+ sc_deps = ScancodeDeps(
1690
+ debug=args.debug, quiet=args.quiet, trace=args.trace, sc_command=args.sc_command, timeout=args.sc_timeout
1691
+ )
1692
+ if not sc_deps.get_dependencies(what_to_scan=args.scan_loc, result_output=args.output):
1693
+ sys.exit(1)
1694
+ return None
610
1695
 
611
1696
 
612
1697
  def convert(parser, args):
@@ -622,27 +1707,437 @@ def convert(parser, args):
622
1707
  if not args.input:
623
1708
  print_stderr('Please specify an input file to convert')
624
1709
  parser.parse_args([args.subparser, '-h'])
625
- exit(1)
1710
+ sys.exit(1)
626
1711
  success = False
627
1712
  if args.format == 'cyclonedx':
628
1713
  if not args.quiet:
629
- print_stderr(f'Producing CycloneDX report...')
1714
+ print_stderr('Producing CycloneDX report...')
630
1715
  cdx = CycloneDx(debug=args.debug, output_file=args.output)
631
1716
  success = cdx.produce_from_file(args.input)
632
1717
  elif args.format == 'spdxlite':
633
1718
  if not args.quiet:
634
- print_stderr(f'Producing SPDX Lite report...')
1719
+ print_stderr('Producing SPDX Lite report...')
635
1720
  spdxlite = SpdxLite(debug=args.debug, output_file=args.output)
636
1721
  success = spdxlite.produce_from_file(args.input)
637
1722
  elif args.format == 'csv':
638
1723
  if not args.quiet:
639
- print_stderr(f'Producing CSV report...')
1724
+ print_stderr('Producing CSV report...')
640
1725
  csvo = CsvOutput(debug=args.debug, output_file=args.output)
641
1726
  success = csvo.produce_from_file(args.input)
1727
+ elif args.format == 'glc-codequality':
1728
+ if not args.quiet:
1729
+ print_stderr('Producing GitLab code quality report...')
1730
+ glc_code_quality = GitLabQualityReport(debug=args.debug, trace=args.trace, quiet=args.quiet)
1731
+ success = glc_code_quality.produce_from_file(args.input, output_file=args.output)
642
1732
  else:
643
1733
  print_stderr(f'ERROR: Unknown output format (--format): {args.format}')
644
1734
  if not success:
645
- exit(1)
1735
+ sys.exit(1)
1736
+
1737
+
1738
+ # =============================================================================
1739
+ # INSPECT COMMAND HANDLERS - Functions that execute inspection operations
1740
+ # =============================================================================
1741
+
1742
+
1743
+ def inspect_copyleft(parser, args):
1744
+ """
1745
+ Handle copyleft license inspection command.
1746
+
1747
+ Analyses scan results to identify components using copyleft licenses
1748
+ that may require compliance actions such as source code disclosure.
1749
+
1750
+ Parameters
1751
+ ----------
1752
+ parser : ArgumentParser
1753
+ Command line parser object for help display
1754
+ args : Namespace
1755
+ Parsed command line arguments containing:
1756
+ - input: Path to scan results file
1757
+ - output: Optional output file path
1758
+ - status: Optional status summary file path
1759
+ - format: Output format (json, md, jira_md)
1760
+ - include/exclude/explicit: License filter options
1761
+ """
1762
+ # Validate required input file parameter
1763
+ if args.input is None:
1764
+ print_stderr('ERROR: Input file is required for copyleft inspection')
1765
+ parser.parse_args([args.subparser, args.subparsercmd, args.subparser_subcmd, '-h'])
1766
+ sys.exit(1)
1767
+ # Initialise output file if specified
1768
+ if args.output:
1769
+ initialise_empty_file(args.output)
1770
+ # Initialise status summary file if specified
1771
+ if args.status:
1772
+ initialise_empty_file(args.status)
1773
+ try:
1774
+ # Create and configure copyleft inspector
1775
+ i_copyleft = Copyleft(
1776
+ debug=args.debug,
1777
+ trace=args.trace,
1778
+ quiet=args.quiet,
1779
+ filepath=args.input,
1780
+ format_type=args.format,
1781
+ status=args.status,
1782
+ output=args.output,
1783
+ include=args.include, # Additional licenses to check
1784
+ exclude=args.exclude, # Licenses to ignore
1785
+ explicit=args.explicit, # Explicit license list
1786
+ license_sources=args.license_sources, # License sources to check (list)
1787
+ )
1788
+ # Execute inspection and exit with appropriate status code
1789
+ status, _ = i_copyleft.run()
1790
+ sys.exit(status)
1791
+ except Exception as e:
1792
+ print_stderr(e)
1793
+ if args.debug:
1794
+ traceback.print_exc()
1795
+ sys.exit(1)
1796
+
1797
+
1798
+ def inspect_undeclared(parser, args):
1799
+ """
1800
+ Handle undeclared components inspection command.
1801
+
1802
+ Analyses scan results to identify components that are present in the
1803
+ codebase but not declared in SBOM or manifest files, which may indicate
1804
+ security or compliance risks.
1805
+
1806
+ Parameters
1807
+ ----------
1808
+ parser : ArgumentParser
1809
+ Command line parser object for help display
1810
+ args : Namespace
1811
+ Parsed command line arguments containing:
1812
+ - input: Path to scan results file
1813
+ - output: Optional output file path
1814
+ - status: Optional status summary file path
1815
+ - format: Output format (json, md, jira_md)
1816
+ - sbom_format: SBOM format type (legacy, settings)
1817
+ """
1818
+ # Validate required input file parameter
1819
+ if args.input is None:
1820
+ print_stderr('ERROR: Input file is required for undeclared component inspection')
1821
+ parser.parse_args([args.subparser, args.subparsercmd, args.subparser_subcmd, '-h'])
1822
+ sys.exit(1)
1823
+
1824
+ # Initialise output file if specified
1825
+ if args.output:
1826
+ initialise_empty_file(args.output)
1827
+
1828
+ # Initialise status summary file if specified
1829
+ if args.status:
1830
+ initialise_empty_file(args.status)
1831
+
1832
+ try:
1833
+ # Create and configure undeclared component inspector
1834
+ i_undeclared = UndeclaredComponent(
1835
+ debug=args.debug,
1836
+ trace=args.trace,
1837
+ quiet=args.quiet,
1838
+ filepath=args.input,
1839
+ format_type=args.format,
1840
+ status=args.status,
1841
+ output=args.output,
1842
+ sbom_format=args.sbom_format, # Format for SBOM comparison
1843
+ )
1844
+
1845
+ # Execute inspection and exit with appropriate status code
1846
+ status, _ = i_undeclared.run()
1847
+ sys.exit(status)
1848
+ except Exception as e:
1849
+ print_stderr(e)
1850
+ if args.debug:
1851
+ traceback.print_exc()
1852
+ sys.exit(1)
1853
+
1854
+
1855
+ def inspect_license_summary(parser, args):
1856
+ """
1857
+ Handle license summary inspection command.
1858
+
1859
+ Generates comprehensive summary of all licenses detected in scan results,
1860
+ including license counts, risk levels, and compliance recommendations.
1861
+
1862
+ Parameters
1863
+ ----------
1864
+ parser : ArgumentParser
1865
+ Command line parser object for help display
1866
+ args : Namespace
1867
+ Parsed command line arguments containing:
1868
+ - input: Path to scan results file
1869
+ - output: Optional output file path
1870
+ - include/exclude/explicit: License filter options
1871
+ """
1872
+ # Validate required input file parameter
1873
+ if args.input is None:
1874
+ print_stderr('ERROR: Input file is required for license summary')
1875
+ parser.parse_args([args.subparser, args.subparsercmd, args.subparser_subcmd, '-h'])
1876
+ sys.exit(1)
1877
+
1878
+ # Initialise output file if specified
1879
+ if args.output:
1880
+ initialise_empty_file(args.output)
1881
+
1882
+ # Create and configure license summary generator
1883
+ i_license_summary = LicenseSummary(
1884
+ debug=args.debug,
1885
+ trace=args.trace,
1886
+ quiet=args.quiet,
1887
+ filepath=args.input,
1888
+ output=args.output,
1889
+ include=args.include, # Additional licenses to include
1890
+ exclude=args.exclude, # Licenses to exclude from summary
1891
+ explicit=args.explicit, # Explicit license list to summarize
1892
+ )
1893
+ try:
1894
+ # Execute summary generation
1895
+ i_license_summary.run()
1896
+ except Exception as e:
1897
+ print_stderr(e)
1898
+ if args.debug:
1899
+ traceback.print_exc()
1900
+ sys.exit(1)
1901
+
1902
+
1903
+ def inspect_component_summary(parser, args):
1904
+ """
1905
+ Handle component summary inspection command.
1906
+
1907
+ Generates a comprehensive summary of all components detected in scan results,
1908
+ including component counts, versions, match types, and security information.
1909
+
1910
+ Parameters
1911
+ ----------
1912
+ parser : ArgumentParser
1913
+ Command line parser object for help display
1914
+ args : Namespace
1915
+ Parsed command line arguments containing:
1916
+ - input: Path to scan results file
1917
+ - output: Optional output file path
1918
+ """
1919
+ # Validate required input file parameter
1920
+ if args.input is None:
1921
+ print_stderr('ERROR: Input file is required for component summary')
1922
+ parser.parse_args([args.subparser, args.subparsercmd, args.subparser_subcmd, '-h'])
1923
+ sys.exit(1)
1924
+
1925
+ # Initialise an output file if specified
1926
+ if args.output:
1927
+ initialise_empty_file(args.output) # Create/clear output file
1928
+
1929
+ # Create and configure component summary generator
1930
+ i_component_summary = ComponentSummary(
1931
+ debug=args.debug,
1932
+ trace=args.trace,
1933
+ quiet=args.quiet,
1934
+ filepath=args.input,
1935
+ output=args.output,
1936
+ )
1937
+
1938
+ try:
1939
+ # Execute summary generation
1940
+ i_component_summary.run()
1941
+ except Exception as e:
1942
+ print_stderr(e)
1943
+ if args.debug:
1944
+ traceback.print_exc()
1945
+ sys.exit(1)
1946
+
1947
+
1948
+ def inspect_dep_track_project_violations(parser, args):
1949
+ """
1950
+ Handle Dependency Track project inspection command.
1951
+
1952
+ Analyses Dependency Track projects for policy violations, security issues,
1953
+ and compliance status. Connects to DT API to retrieve project data and
1954
+ generate detailed violation reports.
1955
+
1956
+ Parameters
1957
+ ----------
1958
+ parser : ArgumentParser
1959
+ Command line parser object for help display
1960
+ args : Namespace
1961
+ Parsed command line arguments containing:
1962
+ - url: Dependency Track base URL
1963
+ - apikey: API key for authentication
1964
+ - project_id: Project UUID to inspect
1965
+ - project_name: Project name to inspect
1966
+ - project_version: Project version to inspect
1967
+ - upload_token: Upload token for project access
1968
+ - output: Optional output file path
1969
+ - format: Output format (json, md)
1970
+ - timeout: Optional timeout for API requests
1971
+
1972
+ """
1973
+ # Make sure we have project id/project name and version
1974
+ _dt_args_validator(parser, args)
1975
+ # Initialise the output file if specified
1976
+ if args.output:
1977
+ initialise_empty_file(args.output)
1978
+ # Create and configure Dependency Track inspector
1979
+ try:
1980
+ dt_proj_violations = DependencyTrackProjectViolationPolicyCheck(
1981
+ debug=args.debug,
1982
+ trace=args.trace,
1983
+ quiet=args.quiet,
1984
+ output=args.output,
1985
+ status=args.status,
1986
+ format_type=args.format,
1987
+ url=args.url, # DT server URL
1988
+ api_key=args.apikey, # Authentication key
1989
+ project_id=args.project_id, # Target project UUID
1990
+ upload_token=args.upload_token, # Upload access token
1991
+ project_name=args.project_name, # DT project name
1992
+ project_version=args.project_version, # DT project version
1993
+ timeout=args.timeout,
1994
+ )
1995
+ # Execute inspection and exit with appropriate status code
1996
+ status = dt_proj_violations.run()
1997
+ sys.exit(status)
1998
+ except Exception as e:
1999
+ print_stderr(e)
2000
+ if args.debug:
2001
+ traceback.print_exc()
2002
+ sys.exit(1)
2003
+
2004
+
2005
+ def inspect_gitlab_matches(parser, args):
2006
+ """
2007
+ Handle GitLab matches the summary inspection command.
2008
+
2009
+ Analyzes SCANOSS scan results and generates a GitLab-compatible Markdown summary
2010
+ report of component matches. The report includes match details, file locations,
2011
+ and optionally clickable links to source files in GitLab repositories.
2012
+
2013
+ This command processes SCANOSS scan output and creates human-readable Markdown.
2014
+
2015
+ Parameters
2016
+ ----------
2017
+ parser : ArgumentParser
2018
+ Command line parser object for help display
2019
+ args : Namespace
2020
+ Parsed command line arguments containing:
2021
+ - input: Path to SCANOSS scan results file (JSON format) to analyze
2022
+ - line_range_prefix: Base URL prefix for generating GitLab file links with line ranges
2023
+ (e.g., 'https://gitlab.com/org/project/-/blob/main')
2024
+ - output: Optional output file path for the generated Markdown report (default: stdout)
2025
+ - debug: Enable debug output for troubleshooting
2026
+ - trace: Enable trace-level logging
2027
+ - quiet: Suppress informational messages
2028
+
2029
+ Notes
2030
+ -----
2031
+ - The output is formatted in Markdown for optimal display in GitLab
2032
+ - Line range prefix enables clickable file references in the report
2033
+ - If output is not specified, the report is written to stdout
2034
+ """
2035
+
2036
+ if args.input is None:
2037
+ parser.parse_args([args.subparser, '-h'])
2038
+ sys.exit(1)
2039
+
2040
+ if args.line_range_prefix is None:
2041
+ parser.parse_args([args.subparser, '-h'])
2042
+ sys.exit(1)
2043
+
2044
+ # Initialize output file if specified (create/truncate)
2045
+ if args.output:
2046
+ initialise_empty_file(args.output)
2047
+
2048
+ try:
2049
+ # Create GitLab matches summary generator with configuration
2050
+ match_summary = MatchSummary(
2051
+ debug=args.debug,
2052
+ trace=args.trace,
2053
+ quiet=args.quiet,
2054
+ scanoss_results_path=args.input, # Path to SCANOSS JSON results
2055
+ output=args.output, # Output file path or None for stdout
2056
+ line_range_prefix=args.line_range_prefix, # GitLab URL prefix for file links
2057
+ )
2058
+
2059
+ # Execute the summary generation
2060
+ match_summary.run()
2061
+ except Exception as e:
2062
+ # Handle any errors during report generation
2063
+ print_stderr(e)
2064
+ if args.debug:
2065
+ traceback.print_exc()
2066
+ sys.exit(1)
2067
+
2068
+
2069
+ # =============================================================================
2070
+ # END INSPECT COMMAND HANDLERS
2071
+ # =============================================================================
2072
+
2073
+
2074
+ def export_dt(parser, args):
2075
+ """
2076
+ Validates and exports a Software Bill of Materials (SBOM) to a Dependency-Track server.
2077
+
2078
+ Parameters:
2079
+ parser (argparse.ArgumentParser): The argument parser to validate input arguments.
2080
+ args (argparse.Namespace): Parsed arguments passed to the command.
2081
+
2082
+ Raises:
2083
+ SystemExit: If argument validation fails or uploading the SBOM to the Dependency-Track server
2084
+ is unsuccessful.
2085
+ """
2086
+ # Make sure we have project id/project name and version
2087
+ _dt_args_validator(parser, args)
2088
+ if args.output:
2089
+ initialise_empty_file(args.output)
2090
+ if not args.quiet:
2091
+ print_stderr(f'Outputting export data result to: {args.output}')
2092
+ try:
2093
+ dt_exporter = DependencyTrackExporter(
2094
+ url=args.url,
2095
+ apikey=args.apikey,
2096
+ output=args.output,
2097
+ debug=args.debug,
2098
+ trace=args.trace,
2099
+ quiet=args.quiet,
2100
+ )
2101
+ success = dt_exporter.upload_sbom_file(
2102
+ args.input, args.project_id, args.project_name, args.project_version, args.output
2103
+ )
2104
+ if not success:
2105
+ sys.exit(1)
2106
+ except Exception as e:
2107
+ print_stderr(f'ERROR: {e}')
2108
+ if args.debug:
2109
+ traceback.print_exc()
2110
+ sys.exit(1)
2111
+
2112
+
2113
+ def _dt_args_validator(parser, args):
2114
+ """
2115
+ Validates command-line arguments related to project identification.
2116
+
2117
+ Parameters
2118
+ ----------
2119
+ parser : argparse.ArgumentParser
2120
+ An argument parser instance for handling command-line arguments.
2121
+ args : argparse.Namespace
2122
+ Parsed arguments from the command line containing project-related information.
2123
+
2124
+ Raises
2125
+ ------
2126
+ SystemExit
2127
+ If neither a project ID nor the required combination of project name and
2128
+ project version is provided, or if any of the compulsory arguments
2129
+ are missing.
2130
+ """
2131
+ if not args.project_id and not args.project_name and not args.project_version:
2132
+ print_stderr(
2133
+ 'Please specify either a project ID (--project-id) or a project name (--project-name) and '
2134
+ 'version (--project-version)'
2135
+ )
2136
+ parser.parse_args([args.subparser, '-h'])
2137
+ sys.exit(1)
2138
+ if not args.project_id and (not args.project_name or not args.project_version):
2139
+ print_stderr('Please supply a project name (--project-name) and version (--project-version)')
2140
+ sys.exit(1)
646
2141
 
647
2142
 
648
2143
  def utils_certloc(*_):
@@ -650,20 +2145,22 @@ def utils_certloc(*_):
650
2145
  Run the "utils certloc" sub-command
651
2146
  :param _: ignored/unused
652
2147
  """
653
- import certifi
2148
+ import certifi # noqa: PLC0415,I001
2149
+
654
2150
  print(f'CA Cert File: {certifi.where()}')
655
2151
 
656
2152
 
657
- def utils_cert_download(_, args):
2153
+ def utils_cert_download(_, args): # pylint: disable=PLR0912 # noqa: PLR0912
658
2154
  """
659
2155
  Run the "utils cert-download" sub-command
660
2156
  :param _: ignore/unused
661
2157
  :param args: Parsed arguments
662
2158
  """
663
- from urllib.parse import urlparse
664
- import socket
665
- from OpenSSL import SSL, crypto
666
- import traceback
2159
+ import socket # noqa: PLC0415,I001
2160
+ import traceback # noqa: PLC0415,I001
2161
+ from urllib.parse import urlparse # noqa: PLC0415,I001
2162
+
2163
+ from OpenSSL import SSL, crypto # noqa: PLC0415,I001
667
2164
 
668
2165
  file = sys.stdout
669
2166
  if args.output:
@@ -680,21 +2177,24 @@ def utils_cert_download(_, args):
680
2177
  certs = conn.get_peer_cert_chain()
681
2178
  for index, cert in enumerate(certs):
682
2179
  cert_components = dict(cert.get_subject().get_components())
683
- if sys.version_info[0] >= 3:
2180
+ if sys.version_info[0] >= PYTHON_MAJOR_VERSION:
684
2181
  cn = cert_components.get(b'CN')
685
2182
  else:
2183
+ # Fallback for Python versions less than PYTHON_MAJOR_VERSION
686
2184
  cn = cert_components.get('CN')
687
2185
  if not args.quiet:
688
- print_stderr(f'Centificate {index} - CN: {cn}')
689
- if sys.version_info[0] >= 3:
690
- print((crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')).strip(), file=file) # Print the downloaded PEM certificate
2186
+ print_stderr(f'Certificate {index} - CN: {cn}')
2187
+ if sys.version_info[0] >= PYTHON_MAJOR_VERSION:
2188
+ print(
2189
+ (crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')).strip(), file=file
2190
+ ) # Print the downloaded PEM certificate
691
2191
  else:
692
2192
  print((crypto.dump_certificate(crypto.FILETYPE_PEM, cert)).strip(), file=file)
693
2193
  except SSL.Error as e:
694
2194
  print_stderr(f'ERROR: Exception ({e.__class__.__name__}) Downloading certificate from {hostname}:{port} - {e}.')
695
2195
  if args.debug:
696
2196
  traceback.print_exc()
697
- exit(1)
2197
+ sys.exit(1)
698
2198
  else:
699
2199
  if args.output:
700
2200
  if args.debug:
@@ -708,14 +2208,15 @@ def utils_pac_proxy(_, args):
708
2208
  :param _: ignore/unused
709
2209
  :param args: Parsed arguments
710
2210
  """
711
- from pypac.resolver import ProxyResolver
2211
+ from pypac.resolver import ProxyResolver # noqa: PLC0415,I001
2212
+
712
2213
  if not args.pac:
713
- print_stderr(f'Error: No pac file option specified.')
714
- exit(1)
2214
+ print_stderr('Error: No pac file option specified.')
2215
+ sys.exit(1)
715
2216
  pac_file = get_pac_file(args.pac)
716
2217
  if pac_file is None:
717
2218
  print_stderr(f'No proxy configuration for: {args.pac}')
718
- exit(1)
2219
+ sys.exit(1)
719
2220
  resolver = ProxyResolver(pac_file)
720
2221
  proxies = resolver.get_proxy_for_requests(args.url)
721
2222
  print(f'Proxies: {proxies}\n')
@@ -732,23 +2233,23 @@ def get_pac_file(pac: str):
732
2233
  if pac == 'auto':
733
2234
  pac_file = pypac.get_pac() # try to determine the PAC file
734
2235
  elif pac.startswith('file://'):
735
- pac_local = pac.strip('file://')
2236
+ pac_local = pac[7:] # Remove 'file://' prefix (7 characters)
736
2237
  if not os.path.exists(pac_local):
737
2238
  print_stderr(f'Error: PAC file does not exist: {pac_local}.')
738
- exit(1)
2239
+ sys.exit(1)
739
2240
  with open(pac_local) as pf:
740
2241
  pac_file = pypac.get_pac(js=pf.read())
741
2242
  elif pac.startswith('http'):
742
2243
  pac_file = pypac.get_pac(url=pac)
743
2244
  else:
744
2245
  print_stderr(f'Error: Unknown PAC file option: {pac}. Should be one of "auto", "file://", "https://"')
745
- exit(1)
2246
+ sys.exit(1)
746
2247
  return pac_file
747
2248
 
748
2249
 
749
- def comp_crypto(parser, args):
2250
+ def crypto_algorithms(parser, args):
750
2251
  """
751
- Run the "component crypto" sub-command
2252
+ Run the "crypto algorithms" sub-command
752
2253
  Parameters
753
2254
  ----------
754
2255
  parser: ArgumentParser
@@ -759,16 +2260,117 @@ def comp_crypto(parser, args):
759
2260
  if (not args.purl and not args.input) or (args.purl and args.input):
760
2261
  print_stderr('Please specify an input file or purl to decorate (--purl or --input)')
761
2262
  parser.parse_args([args.subparser, args.subparsercmd, '-h'])
762
- exit(1)
2263
+ sys.exit(1)
763
2264
  if args.ca_cert and not os.path.exists(args.ca_cert):
764
2265
  print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.')
765
- exit(1)
766
- pac_file = get_pac_file(args.pac)
767
- comps = Components(debug=args.debug, trace=args.trace, quiet=args.quiet, grpc_url=args.api2url, api_key=args.key,
768
- ca_cert=args.ca_cert, proxy=args.proxy, grpc_proxy=args.grpc_proxy, pac=pac_file,
769
- timeout=args.timeout)
770
- if not comps.get_crypto_details(args.input, args.purl, args.output):
771
- exit(1)
2266
+ sys.exit(1)
2267
+
2268
+ try:
2269
+ config = create_cryptography_config_from_args(args)
2270
+ grpc_config = create_grpc_config_from_args(args)
2271
+ if args.pac:
2272
+ grpc_config.pac = get_pac_file(args.pac)
2273
+ if args.header:
2274
+ grpc_config.req_headers = process_req_headers(args.header)
2275
+ client = ScanossGrpc(**asdict(grpc_config))
2276
+
2277
+ cryptography = Cryptography(config=config, client=client)
2278
+ cryptography.get_algorithms()
2279
+ cryptography.present(output_file=args.output)
2280
+ except ScanossGrpcError as e:
2281
+ print_stderr(f'API ERROR: {e}')
2282
+ sys.exit(1)
2283
+ except Exception as e:
2284
+ if args.debug:
2285
+ import traceback # noqa: PLC0415,I001
2286
+
2287
+ traceback.print_exc()
2288
+ print_stderr(f'ERROR: {e}')
2289
+ sys.exit(1)
2290
+
2291
+
2292
+ def crypto_hints(parser, args):
2293
+ """
2294
+ Run the "crypto hints" sub-command
2295
+ Parameters
2296
+ ----------
2297
+ parser: ArgumentParser
2298
+ command line parser object
2299
+ args: Namespace
2300
+ Parsed arguments
2301
+ """
2302
+ if (not args.purl and not args.input) or (args.purl and args.input):
2303
+ print_stderr('Please specify an input file or purl to decorate (--purl or --input)')
2304
+ parser.parse_args([args.subparser, args.subparsercmd, '-h'])
2305
+ sys.exit(1)
2306
+ if args.ca_cert and not os.path.exists(args.ca_cert):
2307
+ print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.')
2308
+ sys.exit(1)
2309
+
2310
+ try:
2311
+ config = create_cryptography_config_from_args(args)
2312
+ grpc_config = create_grpc_config_from_args(args)
2313
+ if args.pac:
2314
+ grpc_config.pac = get_pac_file(args.pac)
2315
+ if args.header:
2316
+ grpc_config.req_headers = process_req_headers(args.header)
2317
+ client = ScanossGrpc(**asdict(grpc_config))
2318
+
2319
+ cryptography = Cryptography(config=config, client=client)
2320
+ cryptography.get_encryption_hints()
2321
+ cryptography.present(output_file=args.output)
2322
+ except ScanossGrpcError as e:
2323
+ print_stderr(f'API ERROR: {e}')
2324
+ sys.exit(1)
2325
+ except Exception as e:
2326
+ if args.debug:
2327
+ import traceback # noqa: PLC0415,I001
2328
+
2329
+ traceback.print_exc()
2330
+ print_stderr(f'ERROR: {e}')
2331
+ sys.exit(1)
2332
+
2333
+
2334
+ def crypto_versions_in_range(parser, args):
2335
+ """
2336
+ Run the "crypto versions-in-range" sub-command
2337
+ Parameters
2338
+ ----------
2339
+ parser: ArgumentParser
2340
+ command line parser object
2341
+ args: Namespace
2342
+ Parsed arguments
2343
+ """
2344
+ if (not args.purl and not args.input) or (args.purl and args.input):
2345
+ print_stderr('Please specify an input file or purl to decorate (--purl or --input)')
2346
+ parser.parse_args([args.subparser, args.subparsercmd, '-h'])
2347
+ sys.exit(1)
2348
+ if args.ca_cert and not os.path.exists(args.ca_cert):
2349
+ print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.')
2350
+ sys.exit(1)
2351
+
2352
+ try:
2353
+ config = create_cryptography_config_from_args(args)
2354
+ grpc_config = create_grpc_config_from_args(args)
2355
+ if args.pac:
2356
+ grpc_config.pac = get_pac_file(args.pac)
2357
+ if args.header:
2358
+ grpc_config.req_headers = process_req_headers(args.header)
2359
+ client = ScanossGrpc(**asdict(grpc_config))
2360
+
2361
+ cryptography = Cryptography(config=config, client=client)
2362
+ cryptography.get_versions_in_range()
2363
+ cryptography.present(output_file=args.output)
2364
+ except ScanossGrpcError as e:
2365
+ print_stderr(f'API ERROR: {e}')
2366
+ sys.exit(1)
2367
+ except Exception as e:
2368
+ if args.debug:
2369
+ import traceback # noqa: PLC0415,I001
2370
+
2371
+ traceback.print_exc()
2372
+ print_stderr(f'ERROR: {e}')
2373
+ sys.exit(1)
772
2374
 
773
2375
 
774
2376
  def comp_vulns(parser, args):
@@ -784,16 +2386,29 @@ def comp_vulns(parser, args):
784
2386
  if (not args.purl and not args.input) or (args.purl and args.input):
785
2387
  print_stderr('Please specify an input file or purl to decorate (--purl or --input)')
786
2388
  parser.parse_args([args.subparser, args.subparsercmd, '-h'])
787
- exit(1)
2389
+ sys.exit(1)
788
2390
  if args.ca_cert and not os.path.exists(args.ca_cert):
789
2391
  print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.')
790
- exit(1)
2392
+ sys.exit(1)
791
2393
  pac_file = get_pac_file(args.pac)
792
- comps = Components(debug=args.debug, trace=args.trace, quiet=args.quiet, grpc_url=args.api2url, api_key=args.key,
793
- ca_cert=args.ca_cert, proxy=args.proxy, grpc_proxy=args.grpc_proxy, pac=pac_file,
794
- timeout=args.timeout)
2394
+ comps = Components(
2395
+ debug=args.debug,
2396
+ trace=args.trace,
2397
+ quiet=args.quiet,
2398
+ grpc_url=args.api2url,
2399
+ api_key=args.key,
2400
+ ca_cert=args.ca_cert,
2401
+ proxy=args.proxy,
2402
+ grpc_proxy=args.grpc_proxy,
2403
+ pac=pac_file,
2404
+ timeout=args.timeout,
2405
+ req_headers=process_req_headers(args.header),
2406
+ ignore_cert_errors=args.ignore_cert_errors,
2407
+ use_grpc=args.grpc,
2408
+ )
795
2409
  if not comps.get_vulnerabilities(args.input, args.purl, args.output):
796
- exit(1)
2410
+ sys.exit(1)
2411
+
797
2412
 
798
2413
  def comp_semgrep(parser, args):
799
2414
  """
@@ -808,16 +2423,28 @@ def comp_semgrep(parser, args):
808
2423
  if (not args.purl and not args.input) or (args.purl and args.input):
809
2424
  print_stderr('Please specify an input file or purl to decorate (--purl or --input)')
810
2425
  parser.parse_args([args.subparser, args.subparsercmd, '-h'])
811
- exit(1)
2426
+ sys.exit(1)
812
2427
  if args.ca_cert and not os.path.exists(args.ca_cert):
813
2428
  print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.')
814
- exit(1)
2429
+ sys.exit(1)
815
2430
  pac_file = get_pac_file(args.pac)
816
- comps = Components(debug=args.debug, trace=args.trace, quiet=args.quiet, grpc_url=args.api2url, api_key=args.key,
817
- ca_cert=args.ca_cert, proxy=args.proxy, grpc_proxy=args.grpc_proxy, pac=pac_file,
818
- timeout=args.timeout)
2431
+ comps = Components(
2432
+ debug=args.debug,
2433
+ trace=args.trace,
2434
+ quiet=args.quiet,
2435
+ grpc_url=args.api2url,
2436
+ api_key=args.key,
2437
+ ca_cert=args.ca_cert,
2438
+ proxy=args.proxy,
2439
+ grpc_proxy=args.grpc_proxy,
2440
+ pac=pac_file,
2441
+ timeout=args.timeout,
2442
+ req_headers=process_req_headers(args.header),
2443
+ use_grpc=args.grpc,
2444
+ )
819
2445
  if not comps.get_semgrep_details(args.input, args.purl, args.output):
820
- exit(1)
2446
+ sys.exit(1)
2447
+
821
2448
 
822
2449
  def comp_search(parser, args):
823
2450
  """
@@ -829,23 +2456,42 @@ def comp_search(parser, args):
829
2456
  args: Namespace
830
2457
  Parsed arguments
831
2458
  """
832
- if ((not args.input and not args.search and not args.vendor and not args.comp) or
833
- (args.input and (args.search or args.vendor or args.comp))):
2459
+ if (not args.input and not args.search and not args.vendor and not args.comp) or (
2460
+ args.input and (args.search or args.vendor or args.comp)
2461
+ ):
834
2462
  print_stderr('Please specify an input file or search terms (--input or --search, or --vendor or --comp.)')
835
2463
  parser.parse_args([args.subparser, args.subparsercmd, '-h'])
836
- exit(1)
2464
+ sys.exit(1)
837
2465
 
838
2466
  if args.ca_cert and not os.path.exists(args.ca_cert):
839
2467
  print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.')
840
- exit(1)
2468
+ sys.exit(1)
841
2469
  pac_file = get_pac_file(args.pac)
842
- comps = Components(debug=args.debug, trace=args.trace, quiet=args.quiet, grpc_url=args.api2url, api_key=args.key,
843
- ca_cert=args.ca_cert, proxy=args.proxy, grpc_proxy=args.grpc_proxy, pac=pac_file,
844
- timeout=args.timeout)
845
- if not comps.search_components(args.output, json_file=args.input,
846
- search=args.search, vendor=args.vendor, comp=args.comp, package=args.package,
847
- limit=args.limit, offset=args.offset):
848
- exit(1)
2470
+ comps = Components(
2471
+ debug=args.debug,
2472
+ trace=args.trace,
2473
+ quiet=args.quiet,
2474
+ grpc_url=args.api2url,
2475
+ api_key=args.key,
2476
+ ca_cert=args.ca_cert,
2477
+ proxy=args.proxy,
2478
+ grpc_proxy=args.grpc_proxy,
2479
+ pac=pac_file,
2480
+ timeout=args.timeout,
2481
+ req_headers=process_req_headers(args.header),
2482
+ use_grpc=args.grpc,
2483
+ )
2484
+ if not comps.search_components(
2485
+ args.output,
2486
+ json_file=args.input,
2487
+ search=args.search,
2488
+ vendor=args.vendor,
2489
+ comp=args.comp,
2490
+ package=args.package,
2491
+ limit=args.limit,
2492
+ offset=args.offset,
2493
+ ):
2494
+ sys.exit(1)
849
2495
 
850
2496
 
851
2497
  def comp_versions(parser, args):
@@ -861,17 +2507,360 @@ def comp_versions(parser, args):
861
2507
  if (not args.input and not args.purl) or (args.input and args.purl):
862
2508
  print_stderr('Please specify an input file or search terms (--input or --purl.)')
863
2509
  parser.parse_args([args.subparser, args.subparsercmd, '-h'])
864
- exit(1)
2510
+ sys.exit(1)
865
2511
 
866
2512
  if args.ca_cert and not os.path.exists(args.ca_cert):
867
2513
  print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.')
868
- exit(1)
2514
+ sys.exit(1)
869
2515
  pac_file = get_pac_file(args.pac)
870
- comps = Components(debug=args.debug, trace=args.trace, quiet=args.quiet, grpc_url=args.api2url, api_key=args.key,
871
- ca_cert=args.ca_cert, proxy=args.proxy, grpc_proxy=args.grpc_proxy, pac=pac_file,
872
- timeout=args.timeout)
2516
+ comps = Components(
2517
+ debug=args.debug,
2518
+ trace=args.trace,
2519
+ quiet=args.quiet,
2520
+ grpc_url=args.api2url,
2521
+ api_key=args.key,
2522
+ ca_cert=args.ca_cert,
2523
+ proxy=args.proxy,
2524
+ grpc_proxy=args.grpc_proxy,
2525
+ pac=pac_file,
2526
+ timeout=args.timeout,
2527
+ req_headers=process_req_headers(args.header),
2528
+ use_grpc=args.grpc,
2529
+ )
873
2530
  if not comps.get_component_versions(args.output, json_file=args.input, purl=args.purl, limit=args.limit):
874
- exit(1)
2531
+ sys.exit(1)
2532
+
2533
+
2534
+ def comp_provenance(parser, args):
2535
+ """
2536
+ Run the "component provenance" sub-command
2537
+ Parameters
2538
+ ----------
2539
+ parser: ArgumentParser
2540
+ command line parser object
2541
+ args: Namespace
2542
+ Parsed arguments
2543
+ """
2544
+ if (not args.purl and not args.input) or (args.purl and args.input):
2545
+ print_stderr('Please specify an input file or purl to decorate (--purl or --input)')
2546
+ parser.parse_args([args.subparser, args.subparsercmd, '-h'])
2547
+ sys.exit(1)
2548
+ if args.ca_cert and not os.path.exists(args.ca_cert):
2549
+ print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.')
2550
+ sys.exit(1)
2551
+ pac_file = get_pac_file(args.pac)
2552
+ comps = Components(
2553
+ debug=args.debug,
2554
+ trace=args.trace,
2555
+ quiet=args.quiet,
2556
+ grpc_url=args.api2url,
2557
+ api_key=args.key,
2558
+ ca_cert=args.ca_cert,
2559
+ proxy=args.proxy,
2560
+ grpc_proxy=args.grpc_proxy,
2561
+ pac=pac_file,
2562
+ timeout=args.timeout,
2563
+ req_headers=process_req_headers(args.header),
2564
+ use_grpc=args.grpc,
2565
+ )
2566
+ if not comps.get_provenance_details(args.input, args.purl, args.output, args.origin):
2567
+ sys.exit(1)
2568
+
2569
+
2570
+ def comp_licenses(parser, args):
2571
+ """
2572
+ Run the "component licenses" sub-command
2573
+ Parameters
2574
+ ----------
2575
+ parser: ArgumentParser
2576
+ command line parser object
2577
+ args: Namespace
2578
+ Parsed arguments
2579
+ """
2580
+ if (not args.purl and not args.input) or (args.purl and args.input):
2581
+ print_stderr('ERROR: Please specify an input file or purl to decorate (--purl or --input)')
2582
+ parser.parse_args([args.subparser, args.subparsercmd, '-h'])
2583
+ sys.exit(1)
2584
+ if args.ca_cert and not os.path.exists(args.ca_cert):
2585
+ print_stderr(f'ERROR: Certificate file does not exist: {args.ca_cert}.')
2586
+ sys.exit(1)
2587
+ pac_file = get_pac_file(args.pac)
2588
+ comps = Components(
2589
+ debug=args.debug,
2590
+ trace=args.trace,
2591
+ quiet=args.quiet,
2592
+ grpc_url=args.api2url,
2593
+ api_key=args.key,
2594
+ ca_cert=args.ca_cert,
2595
+ proxy=args.proxy,
2596
+ grpc_proxy=args.grpc_proxy,
2597
+ pac=pac_file,
2598
+ timeout=args.timeout,
2599
+ req_headers=process_req_headers(args.header),
2600
+ use_grpc=args.grpc,
2601
+ )
2602
+ if not comps.get_licenses(args.input, args.purl, args.output):
2603
+ sys.exit(1)
2604
+
2605
+
2606
+ def results(parser, args):
2607
+ """
2608
+ Run the "results" sub-command
2609
+ Parameters
2610
+ ----------
2611
+ parser: ArgumentParser
2612
+ command line parser object
2613
+ args: Namespace
2614
+ Parsed arguments
2615
+ """
2616
+ if not args.filepath:
2617
+ print_stderr('ERROR: Please specify a file containing the results')
2618
+ parser.parse_args([args.subparser, '-h'])
2619
+ sys.exit(1)
2620
+
2621
+ file_path = Path(args.filepath).resolve()
2622
+
2623
+ if not file_path.is_file():
2624
+ print_stderr(f'The specified file {args.filepath} does not exist')
2625
+ sys.exit(1)
2626
+
2627
+ results = Results(
2628
+ debug=args.debug,
2629
+ trace=args.trace,
2630
+ quiet=args.quiet,
2631
+ filepath=file_path,
2632
+ match_type=args.match_type,
2633
+ status=args.status,
2634
+ output_file=args.output,
2635
+ output_format=args.format,
2636
+ )
2637
+
2638
+ if args.has_pending:
2639
+ results.get_pending_identifications().present()
2640
+ if results.has_results():
2641
+ sys.exit(1)
2642
+ else:
2643
+ results.apply_filters().present()
2644
+
2645
+
2646
+ def process_req_headers(headers_array: List[str]) -> dict:
2647
+ """
2648
+ Process a list of header strings in the format "Name: Value" into a dictionary.
2649
+
2650
+ Args:
2651
+ headers_array (list): List of header strings from command line args
2652
+
2653
+ Returns:
2654
+ dict: Dictionary of header name-value pairs
2655
+ """
2656
+ # Check if headers_array is empty
2657
+ if not headers_array:
2658
+ # Array is empty
2659
+ return {}
2660
+
2661
+ dict_headers = {}
2662
+ for header_str in headers_array:
2663
+ # Split each "Name: Value" header
2664
+ parts = header_str.split(':', 1)
2665
+ if len(parts) == HEADER_PARTS_COUNT:
2666
+ name = parts[0].strip()
2667
+ value = parts[1].strip()
2668
+ dict_headers[name] = value
2669
+ return dict_headers
2670
+
2671
+
2672
+ def folder_hashing_scan(parser, args):
2673
+ """Run the "folder-scan" sub-command
2674
+
2675
+ Args:
2676
+ parser (ArgumentParser): command line parser object
2677
+ args (Namespace): Parsed arguments
2678
+ """
2679
+ try:
2680
+ if not args.scan_dir:
2681
+ print_stderr('ERROR: Please specify a directory to scan')
2682
+ parser.parse_args([args.subparser, '-h'])
2683
+ sys.exit(1)
2684
+
2685
+ if not os.path.exists(args.scan_dir) or not os.path.isdir(args.scan_dir):
2686
+ print_stderr(f'ERROR: The specified directory {args.scan_dir} does not exist')
2687
+ sys.exit(1)
2688
+
2689
+ scanner_config = create_scanner_config_from_args(args)
2690
+ scanoss_settings = get_scanoss_settings_from_args(args)
2691
+ grpc_config = create_grpc_config_from_args(args)
2692
+
2693
+ client = ScanossGrpc(**asdict(grpc_config))
2694
+
2695
+ scanner = ScannerHFH(
2696
+ scan_dir=args.scan_dir,
2697
+ config=scanner_config,
2698
+ client=client,
2699
+ scanoss_settings=scanoss_settings,
2700
+ rank_threshold=args.rank_threshold,
2701
+ depth=args.depth,
2702
+ recursive_threshold=args.recursive_threshold,
2703
+ min_accepted_score=args.min_accepted_score,
2704
+ use_grpc=args.grpc,
2705
+ )
2706
+
2707
+ if scanner.scan():
2708
+ scanner.present(output_file=args.output, output_format=args.format)
2709
+ except ScanossGrpcError as e:
2710
+ print_stderr(f'ERROR: {e}')
2711
+ sys.exit(1)
2712
+
2713
+
2714
+ def folder_hash(parser, args):
2715
+ """Run the "folder-hash" sub-command
2716
+
2717
+ Args:
2718
+ parser (ArgumentParser): command line parser object
2719
+ args (Namespace): Parsed arguments
2720
+ """
2721
+ try:
2722
+ if not args.scan_dir:
2723
+ print_stderr('ERROR: Please specify a directory to scan')
2724
+ parser.parse_args([args.subparser, '-h'])
2725
+ sys.exit(1)
2726
+
2727
+ if not os.path.exists(args.scan_dir) or not os.path.isdir(args.scan_dir):
2728
+ print_stderr(f'ERROR: The specified directory {args.scan_dir} does not exist')
2729
+ sys.exit(1)
2730
+
2731
+ folder_hasher_config = create_folder_hasher_config_from_args(args)
2732
+ scanoss_settings = get_scanoss_settings_from_args(args)
2733
+
2734
+ folder_hasher = FolderHasher(
2735
+ scan_dir=args.scan_dir,
2736
+ config=folder_hasher_config,
2737
+ scanoss_settings=scanoss_settings,
2738
+ depth=args.depth,
2739
+ )
2740
+
2741
+ folder_hasher.hash_directory(args.scan_dir)
2742
+ folder_hasher.present(output_file=args.output, output_format=args.format)
2743
+ except Exception as e:
2744
+ print_stderr(f'ERROR: {e}')
2745
+ sys.exit(1)
2746
+
2747
+
2748
+ def container_scan(parser, args, only_interim_results: bool = False):
2749
+ """
2750
+ Run the "container-scan" sub-command
2751
+ Parameters
2752
+ ----------
2753
+ parser: ArgumentParser
2754
+ command line parser object
2755
+ args: Namespace
2756
+ Parsed arguments
2757
+ """
2758
+ if not args.scan_loc:
2759
+ print_stderr(
2760
+ 'Please specify a container image, Docker tar, OCI tar, OCI directory, SIF Container, or directory to scan'
2761
+ )
2762
+ parser.parse_args([args.subparser, '-h'])
2763
+ sys.exit(1)
2764
+
2765
+ try:
2766
+ config = create_container_scanner_config_from_args(args)
2767
+ config.only_interim_results = only_interim_results
2768
+ container_scanner = ContainerScanner(
2769
+ config=config,
2770
+ what_to_scan=args.scan_loc,
2771
+ )
2772
+
2773
+ container_scanner.scan()
2774
+ if only_interim_results:
2775
+ container_scanner.present(output_file=config.output, output_format='raw')
2776
+ else:
2777
+ container_scanner.decorate_scan_results_with_dependencies()
2778
+ container_scanner.present(output_file=config.output, output_format=config.format)
2779
+ except Exception as e:
2780
+ print_stderr(f'ERROR: {e}')
2781
+ sys.exit(1)
2782
+
2783
+
2784
+ def get_scanoss_settings_from_args(args):
2785
+ scanoss_settings = None
2786
+ if not args.skip_settings_file:
2787
+ scanoss_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet)
2788
+ try:
2789
+ scanoss_settings.load_json_file(args.settings, args.scan_dir).set_file_type('new').set_scan_type('identify')
2790
+ except ScanossSettingsError as e:
2791
+ print_stderr(f'Error: {e}')
2792
+ sys.exit(1)
2793
+ return scanoss_settings
2794
+
2795
+
2796
+ def initialise_empty_file(filename: str):
2797
+ """
2798
+ Initialises an empty file with the specified name. If the file already exists,
2799
+ it truncates its content. Ensures proper error handling in case of failure.
2800
+
2801
+ Args:
2802
+ filename (str): The name of the file to be initialised.
2803
+
2804
+ Raises:
2805
+ SystemExit: If the file cannot be created or written due to an exception,
2806
+ the function prints an error message and exits the program.
2807
+
2808
+ Note:
2809
+ This function writes an empty file and handles exceptions to ensure the
2810
+ program does not continue execution in case of an error.
2811
+ """
2812
+ if filename:
2813
+ try:
2814
+ open(filename, 'w').close()
2815
+ except Exception as e:
2816
+ print_stderr(f'Error: Unable to create output file {filename}: {e}')
2817
+ sys.exit(1)
2818
+
2819
+
2820
+ def delta_copy(parser, args):
2821
+ """
2822
+ Handle delta copy command.
2823
+
2824
+ Copies files listed in an input file to a target directory while preserving
2825
+ their directory structure. Creates a unique delta directory if none is specified.
2826
+
2827
+ Parameters
2828
+ ----------
2829
+ parser : ArgumentParser
2830
+ Command line parser object for help display
2831
+ args : Namespace
2832
+ Parsed command line arguments containing:
2833
+ - input: Path to file containing list of files to copy
2834
+ - folder: Optional target directory path
2835
+ - output: Optional output file path
2836
+ """
2837
+ # Validate required input file parameter
2838
+ if args.input is None:
2839
+ print_stderr('ERROR: Input file is required for copying')
2840
+ parser.parse_args([args.subparser, args.subparsercmd, '-h'])
2841
+ sys.exit(1)
2842
+ # Initialise output file if specified
2843
+ if args.output:
2844
+ initialise_empty_file(args.output)
2845
+ try:
2846
+ # Create and configure delta copy command
2847
+ delta = Delta(
2848
+ debug=args.debug,
2849
+ trace=args.trace,
2850
+ quiet=args.quiet,
2851
+ filepath=args.input,
2852
+ folder=args.folder,
2853
+ output=args.output,
2854
+ root_dir=args.root,
2855
+ )
2856
+ # Execute copy and exit with appropriate status code
2857
+ status, _ = delta.copy()
2858
+ sys.exit(status)
2859
+ except Exception as e:
2860
+ print_stderr(e)
2861
+ if args.debug:
2862
+ traceback.print_exc()
2863
+ sys.exit(1)
875
2864
 
876
2865
 
877
2866
  def main():
@@ -881,5 +2870,5 @@ def main():
881
2870
  setup_args()
882
2871
 
883
2872
 
884
- if __name__ == "__main__":
2873
+ if __name__ == '__main__':
885
2874
  main()