conviso-ast 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. conviso_ast-3.0.0.data/scripts/flow_bash_completer.sh +21 -0
  2. conviso_ast-3.0.0.data/scripts/flow_fish_completer.fish +1 -0
  3. conviso_ast-3.0.0.data/scripts/flow_zsh_completer.sh +32 -0
  4. conviso_ast-3.0.0.dist-info/METADATA +37 -0
  5. conviso_ast-3.0.0.dist-info/RECORD +128 -0
  6. conviso_ast-3.0.0.dist-info/WHEEL +5 -0
  7. conviso_ast-3.0.0.dist-info/entry_points.txt +3 -0
  8. conviso_ast-3.0.0.dist-info/top_level.txt +1 -0
  9. convisoappsec/__init__.py +0 -0
  10. convisoappsec/common/__init__.py +5 -0
  11. convisoappsec/common/box.py +251 -0
  12. convisoappsec/common/cleaner.py +78 -0
  13. convisoappsec/common/docker.py +399 -0
  14. convisoappsec/common/exceptions.py +8 -0
  15. convisoappsec/common/git_data_parser.py +76 -0
  16. convisoappsec/common/graphql/__init__.py +0 -0
  17. convisoappsec/common/graphql/error_handlers.py +75 -0
  18. convisoappsec/common/graphql/errors.py +16 -0
  19. convisoappsec/common/graphql/low_client.py +51 -0
  20. convisoappsec/common/retry_handler.py +40 -0
  21. convisoappsec/common/strings.py +8 -0
  22. convisoappsec/flow/__init__.py +3 -0
  23. convisoappsec/flow/api.py +104 -0
  24. convisoappsec/flow/cleaner.py +118 -0
  25. convisoappsec/flow/graphql_api/__init__.py +0 -0
  26. convisoappsec/flow/graphql_api/beta/__init__.py +0 -0
  27. convisoappsec/flow/graphql_api/beta/client.py +18 -0
  28. convisoappsec/flow/graphql_api/beta/models/__init__.py +0 -0
  29. convisoappsec/flow/graphql_api/beta/models/issues/__init__.py +0 -0
  30. convisoappsec/flow/graphql_api/beta/models/issues/container.py +72 -0
  31. convisoappsec/flow/graphql_api/beta/models/issues/iac.py +6 -0
  32. convisoappsec/flow/graphql_api/beta/models/issues/normalize.py +13 -0
  33. convisoappsec/flow/graphql_api/beta/models/issues/sast.py +53 -0
  34. convisoappsec/flow/graphql_api/beta/models/issues/sca.py +78 -0
  35. convisoappsec/flow/graphql_api/beta/resources_api.py +142 -0
  36. convisoappsec/flow/graphql_api/beta/schemas/__init__.py +0 -0
  37. convisoappsec/flow/graphql_api/beta/schemas/mutations/__init__.py +61 -0
  38. convisoappsec/flow/graphql_api/beta/schemas/resolvers/__init__.py +0 -0
  39. convisoappsec/flow/graphql_api/v1/__init__.py +0 -0
  40. convisoappsec/flow/graphql_api/v1/client.py +46 -0
  41. convisoappsec/flow/graphql_api/v1/models/__init__.py +0 -0
  42. convisoappsec/flow/graphql_api/v1/models/asset.py +14 -0
  43. convisoappsec/flow/graphql_api/v1/models/issues.py +16 -0
  44. convisoappsec/flow/graphql_api/v1/models/project.py +35 -0
  45. convisoappsec/flow/graphql_api/v1/resources_api.py +489 -0
  46. convisoappsec/flow/graphql_api/v1/schemas/__init__.py +0 -0
  47. convisoappsec/flow/graphql_api/v1/schemas/mutations/__init__.py +212 -0
  48. convisoappsec/flow/graphql_api/v1/schemas/resolvers/__init__.py +180 -0
  49. convisoappsec/flow/source_code_scanner/__init__.py +9 -0
  50. convisoappsec/flow/source_code_scanner/exceptions.py +2 -0
  51. convisoappsec/flow/source_code_scanner/scc.py +68 -0
  52. convisoappsec/flow/source_code_scanner/source_code_scanner.py +177 -0
  53. convisoappsec/flow/util/__init__.py +7 -0
  54. convisoappsec/flow/util/ci_provider.py +99 -0
  55. convisoappsec/flow/util/metrics.py +16 -0
  56. convisoappsec/flow/util/source_code_compressor.py +22 -0
  57. convisoappsec/flow/version_control_system_adapter.py +528 -0
  58. convisoappsec/flow/version_searchers/__init__.py +9 -0
  59. convisoappsec/flow/version_searchers/sorted_by_versioning_style.py +85 -0
  60. convisoappsec/flow/version_searchers/timebased_version_seacher.py +39 -0
  61. convisoappsec/flow/version_searchers/version_searcher_result.py +33 -0
  62. convisoappsec/flow/versioning_style/__init__.py +0 -0
  63. convisoappsec/flow/versioning_style/semantic_versioning.py +44 -0
  64. convisoappsec/flowcli/__init__.py +3 -0
  65. convisoappsec/flowcli/__main__.py +4 -0
  66. convisoappsec/flowcli/assets/__init__.py +4 -0
  67. convisoappsec/flowcli/assets/create.py +88 -0
  68. convisoappsec/flowcli/assets/entrypoint.py +20 -0
  69. convisoappsec/flowcli/assets/ls.py +63 -0
  70. convisoappsec/flowcli/ast/__init__.py +3 -0
  71. convisoappsec/flowcli/ast/entrypoint.py +427 -0
  72. convisoappsec/flowcli/common.py +175 -0
  73. convisoappsec/flowcli/companies/__init__.py +0 -0
  74. convisoappsec/flowcli/companies/ls.py +25 -0
  75. convisoappsec/flowcli/container/__init__.py +3 -0
  76. convisoappsec/flowcli/container/entrypoint.py +17 -0
  77. convisoappsec/flowcli/container/run.py +306 -0
  78. convisoappsec/flowcli/context.py +49 -0
  79. convisoappsec/flowcli/deploy/__init__.py +0 -0
  80. convisoappsec/flowcli/deploy/create/__init__.py +4 -0
  81. convisoappsec/flowcli/deploy/create/context.py +12 -0
  82. convisoappsec/flowcli/deploy/create/entrypoint.py +31 -0
  83. convisoappsec/flowcli/deploy/create/with_/__init__.py +3 -0
  84. convisoappsec/flowcli/deploy/create/with_/entrypoint.py +20 -0
  85. convisoappsec/flowcli/deploy/create/with_/tag_tracker/__init__.py +4 -0
  86. convisoappsec/flowcli/deploy/create/with_/tag_tracker/context.py +11 -0
  87. convisoappsec/flowcli/deploy/create/with_/tag_tracker/entrypoint.py +30 -0
  88. convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/__init__.py +4 -0
  89. convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/entrypoint.py +21 -0
  90. convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/time_.py +84 -0
  91. convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/versioning_style.py +115 -0
  92. convisoappsec/flowcli/deploy/create/with_/values.py +133 -0
  93. convisoappsec/flowcli/entrypoint.py +103 -0
  94. convisoappsec/flowcli/environment_checker.py +45 -0
  95. convisoappsec/flowcli/findings/__init__.py +4 -0
  96. convisoappsec/flowcli/findings/create/__init__.py +4 -0
  97. convisoappsec/flowcli/findings/create/entrypoint.py +18 -0
  98. convisoappsec/flowcli/findings/create/with_/__init__.py +3 -0
  99. convisoappsec/flowcli/findings/create/with_/entrypoint.py +19 -0
  100. convisoappsec/flowcli/findings/create/with_/version_tracker.py +93 -0
  101. convisoappsec/flowcli/findings/entrypoint.py +19 -0
  102. convisoappsec/flowcli/findings/import_sarif/__init__.py +4 -0
  103. convisoappsec/flowcli/findings/import_sarif/entrypoint.py +430 -0
  104. convisoappsec/flowcli/help_option.py +18 -0
  105. convisoappsec/flowcli/iac/__init__.py +3 -0
  106. convisoappsec/flowcli/iac/entrypoint.py +17 -0
  107. convisoappsec/flowcli/iac/run.py +328 -0
  108. convisoappsec/flowcli/requirements_verifier.py +132 -0
  109. convisoappsec/flowcli/sast/__init__.py +3 -0
  110. convisoappsec/flowcli/sast/entrypoint.py +17 -0
  111. convisoappsec/flowcli/sast/run.py +485 -0
  112. convisoappsec/flowcli/sbom/__init__.py +3 -0
  113. convisoappsec/flowcli/sbom/entrypoint.py +17 -0
  114. convisoappsec/flowcli/sbom/generate.py +235 -0
  115. convisoappsec/flowcli/sca/__init__.py +3 -0
  116. convisoappsec/flowcli/sca/entrypoint.py +17 -0
  117. convisoappsec/flowcli/sca/run.py +479 -0
  118. convisoappsec/flowcli/vulnerability/__init__.py +3 -0
  119. convisoappsec/flowcli/vulnerability/assert_security_rules.py +201 -0
  120. convisoappsec/flowcli/vulnerability/container_vulnerability_manager.py +175 -0
  121. convisoappsec/flowcli/vulnerability/entrypoint.py +18 -0
  122. convisoappsec/flowcli/vulnerability/rules_schema.json +53 -0
  123. convisoappsec/flowcli/vulnerability/run.py +487 -0
  124. convisoappsec/logger.py +29 -0
  125. convisoappsec/sast/__init__.py +0 -0
  126. convisoappsec/sast/decision.py +45 -0
  127. convisoappsec/sast/sastbox.py +296 -0
  128. convisoappsec/version.py +1 -0
@@ -0,0 +1,479 @@
1
+ import json
2
+ import click
3
+ import click_log
4
+ import traceback
5
+ from convisoappsec.common.retry_handler import RetryHandler
6
+ from convisoappsec.common.box import ContainerWrapper
7
+ from convisoappsec.common import strings
8
+ from convisoappsec.flow.graphql_api.beta.models.issues.sca import CreateScaFindingInput
9
+ from convisoappsec.flowcli import help_option
10
+ from convisoappsec.flowcli.common import asset_id_option, on_http_error
11
+ from convisoappsec.flowcli.context import pass_flow_context
12
+ from convisoappsec.logger import LOGGER, log_and_notify_ast_event
13
+ from convisoappsec.common.graphql.errors import ResponseError
14
+ from convisoappsec.flowcli.requirements_verifier import RequirementsVerifier
15
+ from copy import deepcopy as clone
16
+ from convisoappsec.flowcli.sbom import sbom
17
+ from convisoappsec.flowcli.vulnerability.run import perform_sca_scan, close_sca_issues, reopen_issues
18
+ from convisoappsec.common.cleaner import Cleaner
19
+
20
+ click_log.basic_config(LOGGER)
21
+
22
+
23
+ @click.command()
24
+ @click_log.simple_verbosity_option(LOGGER)
25
+ @asset_id_option(required=False)
26
+ @click.option(
27
+ '-r',
28
+ '--repository-dir',
29
+ default=".",
30
+ show_default=True,
31
+ type=click.Path(
32
+ exists=True,
33
+ resolve_path=True,
34
+ ),
35
+ required=False,
36
+ help="The source code repository directory.",
37
+ )
38
+ @click.option(
39
+ "--send-to-flow/--no-send-to-flow",
40
+ default=True,
41
+ show_default=True,
42
+ required=False,
43
+ help="""Enable or disable the ability of send analysis result
44
+ reports to flow.""",
45
+ hidden=True
46
+ )
47
+ @click.option(
48
+ "--custom-sca-tags",
49
+ hidden=True,
50
+ required=False,
51
+ multiple=True,
52
+ type=(str, str),
53
+ help="""It should be passed as <repository_name> <image_tag>. It accepts multiple values"""
54
+ )
55
+ @click.option(
56
+ "--scanner-timeout",
57
+ hidden=True,
58
+ required=False,
59
+ default=7200,
60
+ type=int,
61
+ help="Set timeout for each scanner"
62
+ )
63
+ @click.option(
64
+ "--parallel-workers",
65
+ hidden=True,
66
+ required=False,
67
+ default=2,
68
+ type=int,
69
+ help="Set max parallel workers"
70
+ )
71
+ @click.option(
72
+ "--deploy-id",
73
+ default=None,
74
+ required=False,
75
+ hidden=True,
76
+ envvar=("CONVISO_DEPLOY_ID", "FLOW_DEPLOY_ID")
77
+ )
78
+ @click.option(
79
+ '--experimental',
80
+ default=False,
81
+ is_flag=True,
82
+ hidden=True,
83
+ help="Enable experimental features.",
84
+ )
85
+ @click.option(
86
+ "--company-id",
87
+ required=False,
88
+ envvar=("CONVISO_COMPANY_ID", "FLOW_COMPANY_ID"),
89
+ help="Company ID on Conviso Platform",
90
+ )
91
+ @click.option(
92
+ '--asset-name',
93
+ required=False,
94
+ envvar=("CONVISO_ASSET_NAME", "FLOW_ASSET_NAME"),
95
+ help="Provides a asset name.",
96
+ )
97
+ @click.option(
98
+ '--vulnerability-auto-close',
99
+ default=False,
100
+ is_flag=True,
101
+ hidden=True,
102
+ help="Enable auto fixing vulnerabilities on cp.",
103
+ )
104
+ @click.option(
105
+ '--from-ast',
106
+ default=False,
107
+ is_flag=True,
108
+ hidden=True,
109
+ help="Internal use only.",
110
+ )
111
+ @click.option(
112
+ '--cleanup',
113
+ default=False,
114
+ is_flag=True,
115
+ show_default=True,
116
+ help="Clean up system resources, including temporary files, stopped containers, unused Docker images and volumes.",
117
+ )
118
+ @click.option(
119
+ '--control-sync-status-id',
120
+ required=False,
121
+ hidden=True,
122
+ help="Control sync status id.",
123
+ )
124
+ @help_option
125
+ @pass_flow_context
126
+ @click.pass_context
127
+ def run(context, flow_context, asset_id, company_id, repository_dir, send_to_flow, custom_sca_tags,
128
+ scanner_timeout, parallel_workers, deploy_id, experimental, asset_name, vulnerability_auto_close, from_ast,
129
+ cleanup, control_sync_status_id):
130
+ """
131
+ This command will perform SCA analysis at the source code. The analysis
132
+ results can be reported or not to flow application.
133
+ """
134
+ if not from_ast:
135
+ prepared_context = RequirementsVerifier.prepare_context(clone(context))
136
+
137
+ params_to_copy = [
138
+ 'asset_id', 'company_id', 'repository_dir', 'send_to_flow',
139
+ 'deploy_id', 'custom_sca_tags', 'scanner_timeout', 'parallel_workers',
140
+ 'experimental', 'asset_name', 'vulnerability_auto_close', 'cleanup', 'control_sync_status_id'
141
+ ]
142
+
143
+ for param_name in params_to_copy:
144
+ context.params[param_name] = (
145
+ locals()[param_name] or prepared_context.params[param_name]
146
+ )
147
+
148
+ perform_command(
149
+ flow_context,
150
+ context,
151
+ context.params['asset_id'],
152
+ context.params['company_id'],
153
+ context.params['repository_dir'],
154
+ context.params['send_to_flow'],
155
+ context.params['custom_sca_tags'],
156
+ context.params['scanner_timeout'],
157
+ context.params['deploy_id'],
158
+ context.params['experimental'],
159
+ from_ast,
160
+ context.params['cleanup'],
161
+ context.params['control_sync_status_id']
162
+ )
163
+
164
+ def deploy_results_to_conviso(flow_context, conviso_api, results_filepaths, asset_id, company_id, control_sync_status_id):
165
+ """Send vulnerabilities to Conviso platform via GraphQL endpoint."""
166
+
167
+ results_context = click.progressbar(
168
+ results_filepaths, label="Sending SCA reports to the Conviso Platform..."
169
+ )
170
+
171
+ duplicated_issues = 0
172
+ total_issues = 0
173
+
174
+ with results_context as reports:
175
+ for report_path in reports:
176
+ try:
177
+ with open(report_path, 'r') as report_file:
178
+ report_content = json.load(report_file)
179
+
180
+ issues = report_content.get("issues", [])
181
+
182
+ for issue in issues:
183
+ if not issue:
184
+ continue
185
+
186
+ total_issues += 1
187
+ description = issue.get("description", "")
188
+ hash_issue = issue.get('hash_issue', [])
189
+ cves = next(([item] for item in issue.get("cve", []) if item.startswith("CVE")), [])
190
+ path = issue.get("path", "")
191
+ fixed_version = issue.get('fixed_version', {})
192
+ patched_version = fixed_version.get('fixed') if fixed_version else None
193
+ description = description or ""
194
+ sanitezed_description = strings.parse_to_ascii(description)
195
+ severity = define_severity(issue.get("severity", ""))
196
+
197
+ issue_model = CreateScaFindingInput(
198
+ asset_id=asset_id,
199
+ title=issue.get("title", ""),
200
+ description=sanitezed_description,
201
+ severity=severity,
202
+ solution=issue.get("solution", ""),
203
+ reference=parse_conviso_references(issue.get("references", [])),
204
+ file_name=get_relative_path(path),
205
+ affected_version=issue.get("version", "Unknown"),
206
+ package=issue.get("component", "Unknown"),
207
+ cve=cves,
208
+ patched_version=patched_version,
209
+ category=issue.get('cwe', ''),
210
+ original_issue_id_from_tool=hash_issue,
211
+ control_sync_status_id=control_sync_status_id
212
+ )
213
+
214
+ try:
215
+ conviso_api.issues.create_sca(issue_model)
216
+ except ResponseError as error:
217
+ if error.code == 'RECORD_NOT_UNIQUE':
218
+ duplicated_issues += 1
219
+ else:
220
+ retry_handler = RetryHandler(
221
+ flow_context=flow_context, company_id=company_id, asset_id=asset_id
222
+ )
223
+ retry_handler.execute_with_retry(conviso_api.issues.create_sca, issue_model)
224
+
225
+ except Exception:
226
+ retry_handler = RetryHandler(
227
+ flow_context=flow_context, company_id=company_id, asset_id=asset_id
228
+ )
229
+ retry_handler.execute_with_retry(conviso_api.issues.create_sca, issue_model)
230
+
231
+ continue
232
+
233
+ except (OSError, json.JSONDecodeError):
234
+ LOGGER.warn(f"⚠️ Failed to process the report. Our technical team has been notified.")
235
+ full_trace = traceback.format_exc()
236
+ log_and_notify_ast_event(
237
+ flow_context=flow_context, company_id=company_id, asset_id=asset_id,
238
+ ast_log=str(full_trace)
239
+ )
240
+ continue
241
+
242
+ LOGGER.info(f"💬 {duplicated_issues} issue(s) ignored due to duplication.")
243
+
244
+ def define_severity(osv_severity):
245
+ """Map OSV severity levels to Conviso platform severity levels."""
246
+ mapping = {
247
+ "LOW": "LOW",
248
+ "MODERATE": "MEDIUM",
249
+ "HIGH": "HIGH",
250
+ "CRITICAL": "CRITICAL",
251
+ }
252
+
253
+ return mapping.get(osv_severity.upper(), "LOW")
254
+
255
+ def parse_conviso_references(references=[]):
256
+ DIVIDER = "\n"
257
+ urls = [ref['url'] for ref in references]
258
+ return DIVIDER.join(urls)
259
+
260
+
261
+ def get_relative_path(path):
262
+ """
263
+ Returns the full path if the file is in a subdirectory or just the file name if it's in the root directory,
264
+ disregarding the '/code/' prefix.
265
+
266
+ :param path: The file path.
267
+ :return: The processed path.
268
+ """
269
+
270
+ if not path:
271
+ return ''
272
+
273
+ if path.startswith('/code/'):
274
+ relative_path = path[len('/code/'):]
275
+ else:
276
+ relative_path = path
277
+
278
+ if '/' in relative_path:
279
+ return relative_path
280
+ else:
281
+ return relative_path.split('/')[-1]
282
+
283
+
284
+ def perform_command(
285
+ flow_context, context, asset_id, company_id, repository_dir, send_to_flow, custom_sca_tags, scanner_timeout,
286
+ deploy_id, experimental, from_ast, cleanup, control_sync_status_id
287
+ ):
288
+ if send_to_flow and experimental and not asset_id:
289
+ raise click.MissingParameter(
290
+ "It is required when sending reports to Conviso Platform using experimental API.",
291
+ param_type="option",
292
+ param_hint="--asset-id",
293
+ )
294
+
295
+ try:
296
+ REQUIRED_CODEBASE_PATH = '/code'
297
+ OSV_SCANNER_IMAGE_NAME = 'osv_scanner'
298
+
299
+ scanners = {
300
+ OSV_SCANNER_IMAGE_NAME: {
301
+ 'repository_name': OSV_SCANNER_IMAGE_NAME,
302
+ 'tag': 'latest',
303
+ 'command': [
304
+ '-c', REQUIRED_CODEBASE_PATH,
305
+ '-f', 'json',
306
+ '-o', '/{}.json'.format(OSV_SCANNER_IMAGE_NAME)
307
+ ],
308
+ 'repository_dir': repository_dir
309
+ },
310
+ }
311
+
312
+ if custom_sca_tags:
313
+ for custom_tag in custom_sca_tags:
314
+ scan_name, tag = custom_tag
315
+ if scan_name in scanners.keys():
316
+ scanners[scan_name]['tag'] = tag
317
+ else:
318
+ raise click.BadOptionUsage(
319
+ option_name='--custom-sca-tags',
320
+ message="Custom scan {0} or tag {1} invalid".format(
321
+ scan_name, tag)
322
+ )
323
+
324
+ conviso_rest_api = flow_context.create_conviso_rest_api_client()
325
+ token = conviso_rest_api.docker_registry.get_sast_token()
326
+ LOGGER.info('💬 Preparing Environment...')
327
+ scabox = ContainerWrapper(
328
+ token=token,
329
+ containers_map=scanners,
330
+ logger=LOGGER,
331
+ timeout=scanner_timeout
332
+ )
333
+ LOGGER.info('💬 Starting SCA...')
334
+ scabox.run()
335
+
336
+ LOGGER.info('💬 Processing Results...')
337
+ results_filepaths = []
338
+ for unit in scabox.scanners:
339
+ file_path = unit.results
340
+ if file_path:
341
+ results_filepaths.append(file_path)
342
+
343
+ if send_to_flow:
344
+ LOGGER.info("Sending data to the Conviso Platform...")
345
+ conviso_beta_api = flow_context.create_conviso_api_client_beta()
346
+
347
+ deploy_results_to_conviso(flow_context, conviso_beta_api, results_filepaths, asset_id, company_id, control_sync_status_id)
348
+
349
+ # TODO add CI Decision block code
350
+ LOGGER.info('✅ SCA Scan Finished.')
351
+
352
+ # Generate SBOM when execute a sca only scan.
353
+ sbom_generate = sbom.commands.get('generate')
354
+ context.params.pop('cleanup', None)
355
+ specific_param = {"from_ast": True}
356
+ context.params.update(specific_param)
357
+ sbom_generate.invoke(context)
358
+
359
+ # run auto close for sca run
360
+ if context.params['vulnerability_auto_close'] is True and from_ast == False:
361
+ log_func("[*] Verifying if any vulnerability was fixed...")
362
+ try:
363
+ perform_sca_auto_close(flow_context, company_id, asset_id, repository_dir)
364
+ except Exception:
365
+ LOGGER.warn(f"⚠️ Failed to execute vulnerability auto close. Our technical team has been notified.")
366
+ full_trace = traceback.format_exc()
367
+ log_and_notify_ast_event(
368
+ flow_context=flow_context, company_id=company_id, asset_id=asset_id,
369
+ ast_log=str(full_trace)
370
+ )
371
+
372
+ if cleanup and from_ast == False:
373
+ LOGGER.info("🧹 Cleaning up ...")
374
+ cleaner = Cleaner()
375
+ cleaner.cleanup()
376
+
377
+ if not results_filepaths:
378
+ context.params['sca_vulnerability_count'] = 0
379
+ return
380
+
381
+ context.params['sca_vulnerability_count'] = total_vulnerability_count(results_filepaths[0])
382
+
383
+ except Exception as e:
384
+ on_http_error(e)
385
+ raise click.ClickException(str(e)) from e
386
+
387
+
388
+ def total_vulnerability_count(file_path: str) -> int:
389
+ """
390
+ Extract the total vulnerability count from a sca scan result file.
391
+
392
+ Args:
393
+ file_path (str): Path to JSON result file containing vulnerability scan results.
394
+ The file should have a 'summary' section with
395
+ 'issues_count.total' field.
396
+
397
+ Returns:
398
+ int: Total number of vulnerabilities found in the scan result file.
399
+ Returns 0 if the file doesn't exist or no vulnerabilities are found.
400
+ """
401
+
402
+ try:
403
+ with open(file_path, 'r', encoding='utf-8') as f:
404
+ data = json.load(f)
405
+ return len(data.get('issues', []))
406
+
407
+ except (FileNotFoundError, KeyError, json.JSONDecodeError):
408
+ return 0
409
+
410
+
411
+ def perform_sca_auto_close(flow_context, company_id, asset_id, repository_dir):
412
+ """ This method perform auto close vulnerabilities for sca only """
413
+ conviso_beta_api = flow_context.create_conviso_api_client_beta()
414
+ statuses = ['CREATED', 'IDENTIFIED', 'IN_PROGRESS', 'AWAITING_VALIDATION', 'FIX_ACCEPTED']
415
+ page = 1
416
+ merged_issues_sca = []
417
+
418
+ # get vulnerabilities until last page
419
+ while True:
420
+ issues_from_cp = conviso_beta_api.issues.auto_close_vulnerabilities(
421
+ company_id, asset_id, statuses, page, vulnerability_type='SCA_FINDING'
422
+ )
423
+
424
+ total_pages = issues_from_cp['metadata']['totalPages']
425
+ issues_collection = issues_from_cp['collection']
426
+ issues_collection = [item for item in issues_collection if item['scanSource'] == 'conviso_scanner']
427
+
428
+ merged_issues_sca.extend(issues_collection)
429
+
430
+ if total_pages == page:
431
+ break
432
+ else:
433
+ page += 1
434
+
435
+ sca_issues_with_fix_accepted = [item for item in merged_issues_sca if item['status'] == 'FIX_ACCEPTED']
436
+ sca_issues_without_fix_accepted = [item for item in merged_issues_sca if item['status'] != 'FIX_ACCEPTED']
437
+
438
+ if len(issues_from_cp) == 0:
439
+ log_func("No vulnerabilities were found on the Conviso Platform!")
440
+ return
441
+
442
+ sca_hash_issues = perform_sca_scan(repository_dir=repository_dir)
443
+
444
+ set_of_sca_hash_issues = set(sca_hash_issues)
445
+ close_sca_issues(conviso_beta_api, sca_issues_without_fix_accepted, set_of_sca_hash_issues)
446
+
447
+ sca_issues_to_reopen = [
448
+ {'id': item['id'], 'originalIssueIdFromTool': item['originalIssueIdFromTool']}
449
+ for item in sca_issues_with_fix_accepted if item['originalIssueIdFromTool'] in sca_hash_issues
450
+ ]
451
+
452
+ if sca_issues_to_reopen:
453
+ log_func("SCA: reopening {issues} vulnerability/vulnerabilities on conviso platform ...".format(
454
+ issues=len(sca_issues_to_reopen))
455
+ )
456
+
457
+ reopen_issues(conviso_beta_api, sca_issues_to_reopen)
458
+
459
+ def log_func(msg, new_line=True):
460
+ click.echo(click.style(msg, bold=True, fg='blue'), nl=new_line, err=True)
461
+
462
+ EPILOG = '''
463
+ Examples:
464
+
465
+ \b
466
+ 1 - Reporting the results to flow api:
467
+ 1.1 - Running an analysis at all commit range:
468
+ $ export CONVISO_API_KEY='your-api-key'
469
+ $ {command}
470
+
471
+ ''' # noqa: E501
472
+
473
+ SHORT_HELP = "Perform Source Composition analysis"
474
+
475
+ command = 'conviso sca run'
476
+ run.short_help = SHORT_HELP
477
+ run.epilog = EPILOG.format(
478
+ command=command,
479
+ )
@@ -0,0 +1,3 @@
1
+ from .entrypoint import vulnerability
2
+
3
+ __all__ = ['vulnerability']
@@ -0,0 +1,201 @@
1
+ import click
2
+ import click_log
3
+ import logging
4
+ import yaml
5
+ import json
6
+ import jsonschema
7
+ from datetime import datetime, timedelta
8
+ from pkg_resources import resource_filename
9
+ from convisoappsec.flowcli import help_option
10
+ from convisoappsec.flowcli.context import pass_flow_context
11
+ from convisoappsec.flowcli.common import asset_id_option
12
+ from convisoappsec.flowcli.requirements_verifier import RequirementsVerifier
13
+ from convisoappsec.flowcli.companies.ls import Companies
14
+
15
+ logger = logging.getLogger(__name__)
16
+ click_log.basic_config(logger)
17
+
18
+
19
+ @click.command('assert-security-rules')
20
+ @click_log.simple_verbosity_option(logger)
21
+ @asset_id_option(
22
+ required=False
23
+ )
24
+ @click.option(
25
+ "--company-id",
26
+ required=False,
27
+ envvar=("CONVISO_COMPANY_ID", "FLOW_COMPANY_ID"),
28
+ help="Company ID on Conviso Platform",
29
+ )
30
+ @click.option(
31
+ "-r",
32
+ "--repository-dir",
33
+ default=".",
34
+ show_default=True,
35
+ type=click.Path(
36
+ exists=True,
37
+ resolve_path=True,
38
+ ),
39
+ required=False,
40
+ help="The source code repository directory.",
41
+ )
42
+ @click.option(
43
+ '--rules-file',
44
+ 'rules_file',
45
+ type=click.File('r'),
46
+ required=True
47
+ )
48
+ @click.option(
49
+ '--asset-name',
50
+ required=False,
51
+ envvar=("CONVISO_ASSET_NAME", "FLOW_ASSET_NAME"),
52
+ help="Provides a asset name.",
53
+ )
54
+ @help_option
55
+ @pass_flow_context
56
+ @click.pass_context
57
+ def assert_security_rules(
58
+ context, flow_context, asset_id, company_id, repository_dir, rules_file, asset_name
59
+ ):
60
+ prepared_context = RequirementsVerifier.prepare_context(context)
61
+ asset_id = prepared_context.params['asset_id']
62
+
63
+ if company_id is None:
64
+ companies = Companies()
65
+ company = companies.ls(flow_context, company_id=company_id)
66
+ company_id = company[0]['id']
67
+
68
+ try:
69
+ rules = yaml.load(
70
+ rules_file,
71
+ Loader=yaml.Loader
72
+ )
73
+
74
+ click.secho(
75
+ '💬 Starting vulnerability security rules assertion...',
76
+ bold=True
77
+ )
78
+
79
+ click.secho(
80
+ "💬 Applying the given rules at the security gate:\n{0}".format(yaml.dump(rules)),
81
+ bold=True
82
+ )
83
+
84
+ conviso_api = flow_context.create_conviso_graphql_client()
85
+ statuses = ['IDENTIFIED', 'IN_PROGRESS', 'AWAITING_VALIDATION']
86
+
87
+ if validate_json(rules)[0] is False:
88
+ msg = click.secho(
89
+ '💬 Error: Validation of the security gate YAML file failed during the validation step!',
90
+ bold=True
91
+ )
92
+
93
+ return msg
94
+
95
+ current_date = datetime.now()
96
+ default_end_date = datetime(
97
+ current_date.year, current_date.month, current_date.day, 23, 59, 59
98
+ )
99
+
100
+ tolerated_days_exist = any(
101
+ 'max_days_to_fix' in severity for rule in rules['rules'] for severity in rule['severity'].values()
102
+ )
103
+
104
+ issues_set = set()
105
+
106
+ if tolerated_days_exist:
107
+ tolerated_days_and_severity = tolerated_days_by_severity(rules)
108
+ for severity, days in tolerated_days_and_severity.items():
109
+ end_date = default_end_date - timedelta(days=days)
110
+ end_date = end_date.isoformat() + "Z"
111
+ severity_issues = conviso_api.issues.get_issues_stats(
112
+ asset_id, company_id, statuses, end_date=end_date
113
+ )
114
+ for issue in severity_issues:
115
+ severity = severity.upper()
116
+
117
+ if issue['value'] == severity:
118
+ issues_set.add((severity, issue["count"]))
119
+
120
+ issues = [{'value': value, 'count': count} for value, count in issues_set]
121
+ else:
122
+ issues = conviso_api.issues.get_issues_stats(asset_id, company_id, statuses)
123
+
124
+ response = validate_rules(issues, rules)
125
+
126
+ __raise_if_gate_locked(response)
127
+
128
+ click.secho(
129
+ '✅ Vulnerability security rules assertion finished.',
130
+ bold=True
131
+ )
132
+ except Exception as e:
133
+ raise click.ClickException(str(e)) from e
134
+
135
+
136
+ def tolerated_days_by_severity(rules):
137
+ """
138
+ Returns tolerated days for each severity to facilitate validation.
139
+ """
140
+ days_by_severity = {}
141
+
142
+ for rule in rules['rules']:
143
+ for severity, values in rule['severity'].items():
144
+ if 'max_days_to_fix' in values:
145
+ days_by_severity[severity] = values['max_days_to_fix']
146
+ else:
147
+ days_by_severity[severity] = 0
148
+
149
+ return days_by_severity
150
+
151
+
152
+ def __raise_if_gate_locked(response):
153
+ if response['locked']:
154
+ click.secho('💬 Vulnerabilities summary...', bold=True)
155
+
156
+ logger.info(
157
+ json.dumps(response['summary'], indent=4)
158
+ )
159
+
160
+ raise click.ClickException(
161
+ 'Vulnerabilities quantity offending security rules.'
162
+ )
163
+
164
+
165
+ def validate_rules(issues, rules):
166
+ """ function to validate security gate rules """
167
+ response = {"locked": False, "summary": [{"from": "any", "severity": {}}]}
168
+ parsed_issues = {issue['value']: issue['count'] for issue in issues}
169
+
170
+ for i, rule in enumerate(rules['rules']):
171
+ for criticity, rule_max in rule['severity'].items():
172
+ if parsed_issues[criticity.upper()] > rule_max['maximum']:
173
+ response['locked'] = True
174
+ response["summary"][i]["severity"].update({criticity: {"quantity": parsed_issues[criticity.upper()]}})
175
+
176
+ return response
177
+
178
+
179
+ def validate_json(rules):
180
+ """ Validate a JSON document against a JSON schema. """
181
+ schema_path = resource_filename('convisoappsec', 'flowcli/vulnerability/rules_schema.json')
182
+
183
+ with open(schema_path, 'r') as json_file:
184
+ schema = json.load(json_file)
185
+ try:
186
+ jsonschema.validate(rules, schema)
187
+ return True, "Validation successful"
188
+ except jsonschema.exceptions.ValidationError as e:
189
+ return False, str(e)
190
+
191
+
192
+ EPILOG = '''
193
+ '''
194
+
195
+ SHORT_HELP = ''
196
+
197
+ command = 'conviso vulnerability assert-security-rules'
198
+ assert_security_rules.short_help = SHORT_HELP
199
+ assert_security_rules.epilog = EPILOG.format(
200
+ command=command,
201
+ )