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.
- conviso_ast-3.0.0.data/scripts/flow_bash_completer.sh +21 -0
- conviso_ast-3.0.0.data/scripts/flow_fish_completer.fish +1 -0
- conviso_ast-3.0.0.data/scripts/flow_zsh_completer.sh +32 -0
- conviso_ast-3.0.0.dist-info/METADATA +37 -0
- conviso_ast-3.0.0.dist-info/RECORD +128 -0
- conviso_ast-3.0.0.dist-info/WHEEL +5 -0
- conviso_ast-3.0.0.dist-info/entry_points.txt +3 -0
- conviso_ast-3.0.0.dist-info/top_level.txt +1 -0
- convisoappsec/__init__.py +0 -0
- convisoappsec/common/__init__.py +5 -0
- convisoappsec/common/box.py +251 -0
- convisoappsec/common/cleaner.py +78 -0
- convisoappsec/common/docker.py +399 -0
- convisoappsec/common/exceptions.py +8 -0
- convisoappsec/common/git_data_parser.py +76 -0
- convisoappsec/common/graphql/__init__.py +0 -0
- convisoappsec/common/graphql/error_handlers.py +75 -0
- convisoappsec/common/graphql/errors.py +16 -0
- convisoappsec/common/graphql/low_client.py +51 -0
- convisoappsec/common/retry_handler.py +40 -0
- convisoappsec/common/strings.py +8 -0
- convisoappsec/flow/__init__.py +3 -0
- convisoappsec/flow/api.py +104 -0
- convisoappsec/flow/cleaner.py +118 -0
- convisoappsec/flow/graphql_api/__init__.py +0 -0
- convisoappsec/flow/graphql_api/beta/__init__.py +0 -0
- convisoappsec/flow/graphql_api/beta/client.py +18 -0
- convisoappsec/flow/graphql_api/beta/models/__init__.py +0 -0
- convisoappsec/flow/graphql_api/beta/models/issues/__init__.py +0 -0
- convisoappsec/flow/graphql_api/beta/models/issues/container.py +72 -0
- convisoappsec/flow/graphql_api/beta/models/issues/iac.py +6 -0
- convisoappsec/flow/graphql_api/beta/models/issues/normalize.py +13 -0
- convisoappsec/flow/graphql_api/beta/models/issues/sast.py +53 -0
- convisoappsec/flow/graphql_api/beta/models/issues/sca.py +78 -0
- convisoappsec/flow/graphql_api/beta/resources_api.py +142 -0
- convisoappsec/flow/graphql_api/beta/schemas/__init__.py +0 -0
- convisoappsec/flow/graphql_api/beta/schemas/mutations/__init__.py +61 -0
- convisoappsec/flow/graphql_api/beta/schemas/resolvers/__init__.py +0 -0
- convisoappsec/flow/graphql_api/v1/__init__.py +0 -0
- convisoappsec/flow/graphql_api/v1/client.py +46 -0
- convisoappsec/flow/graphql_api/v1/models/__init__.py +0 -0
- convisoappsec/flow/graphql_api/v1/models/asset.py +14 -0
- convisoappsec/flow/graphql_api/v1/models/issues.py +16 -0
- convisoappsec/flow/graphql_api/v1/models/project.py +35 -0
- convisoappsec/flow/graphql_api/v1/resources_api.py +489 -0
- convisoappsec/flow/graphql_api/v1/schemas/__init__.py +0 -0
- convisoappsec/flow/graphql_api/v1/schemas/mutations/__init__.py +212 -0
- convisoappsec/flow/graphql_api/v1/schemas/resolvers/__init__.py +180 -0
- convisoappsec/flow/source_code_scanner/__init__.py +9 -0
- convisoappsec/flow/source_code_scanner/exceptions.py +2 -0
- convisoappsec/flow/source_code_scanner/scc.py +68 -0
- convisoappsec/flow/source_code_scanner/source_code_scanner.py +177 -0
- convisoappsec/flow/util/__init__.py +7 -0
- convisoappsec/flow/util/ci_provider.py +99 -0
- convisoappsec/flow/util/metrics.py +16 -0
- convisoappsec/flow/util/source_code_compressor.py +22 -0
- convisoappsec/flow/version_control_system_adapter.py +528 -0
- convisoappsec/flow/version_searchers/__init__.py +9 -0
- convisoappsec/flow/version_searchers/sorted_by_versioning_style.py +85 -0
- convisoappsec/flow/version_searchers/timebased_version_seacher.py +39 -0
- convisoappsec/flow/version_searchers/version_searcher_result.py +33 -0
- convisoappsec/flow/versioning_style/__init__.py +0 -0
- convisoappsec/flow/versioning_style/semantic_versioning.py +44 -0
- convisoappsec/flowcli/__init__.py +3 -0
- convisoappsec/flowcli/__main__.py +4 -0
- convisoappsec/flowcli/assets/__init__.py +4 -0
- convisoappsec/flowcli/assets/create.py +88 -0
- convisoappsec/flowcli/assets/entrypoint.py +20 -0
- convisoappsec/flowcli/assets/ls.py +63 -0
- convisoappsec/flowcli/ast/__init__.py +3 -0
- convisoappsec/flowcli/ast/entrypoint.py +427 -0
- convisoappsec/flowcli/common.py +175 -0
- convisoappsec/flowcli/companies/__init__.py +0 -0
- convisoappsec/flowcli/companies/ls.py +25 -0
- convisoappsec/flowcli/container/__init__.py +3 -0
- convisoappsec/flowcli/container/entrypoint.py +17 -0
- convisoappsec/flowcli/container/run.py +306 -0
- convisoappsec/flowcli/context.py +49 -0
- convisoappsec/flowcli/deploy/__init__.py +0 -0
- convisoappsec/flowcli/deploy/create/__init__.py +4 -0
- convisoappsec/flowcli/deploy/create/context.py +12 -0
- convisoappsec/flowcli/deploy/create/entrypoint.py +31 -0
- convisoappsec/flowcli/deploy/create/with_/__init__.py +3 -0
- convisoappsec/flowcli/deploy/create/with_/entrypoint.py +20 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/__init__.py +4 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/context.py +11 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/entrypoint.py +30 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/__init__.py +4 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/entrypoint.py +21 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/time_.py +84 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/versioning_style.py +115 -0
- convisoappsec/flowcli/deploy/create/with_/values.py +133 -0
- convisoappsec/flowcli/entrypoint.py +103 -0
- convisoappsec/flowcli/environment_checker.py +45 -0
- convisoappsec/flowcli/findings/__init__.py +4 -0
- convisoappsec/flowcli/findings/create/__init__.py +4 -0
- convisoappsec/flowcli/findings/create/entrypoint.py +18 -0
- convisoappsec/flowcli/findings/create/with_/__init__.py +3 -0
- convisoappsec/flowcli/findings/create/with_/entrypoint.py +19 -0
- convisoappsec/flowcli/findings/create/with_/version_tracker.py +93 -0
- convisoappsec/flowcli/findings/entrypoint.py +19 -0
- convisoappsec/flowcli/findings/import_sarif/__init__.py +4 -0
- convisoappsec/flowcli/findings/import_sarif/entrypoint.py +430 -0
- convisoappsec/flowcli/help_option.py +18 -0
- convisoappsec/flowcli/iac/__init__.py +3 -0
- convisoappsec/flowcli/iac/entrypoint.py +17 -0
- convisoappsec/flowcli/iac/run.py +328 -0
- convisoappsec/flowcli/requirements_verifier.py +132 -0
- convisoappsec/flowcli/sast/__init__.py +3 -0
- convisoappsec/flowcli/sast/entrypoint.py +17 -0
- convisoappsec/flowcli/sast/run.py +485 -0
- convisoappsec/flowcli/sbom/__init__.py +3 -0
- convisoappsec/flowcli/sbom/entrypoint.py +17 -0
- convisoappsec/flowcli/sbom/generate.py +235 -0
- convisoappsec/flowcli/sca/__init__.py +3 -0
- convisoappsec/flowcli/sca/entrypoint.py +17 -0
- convisoappsec/flowcli/sca/run.py +479 -0
- convisoappsec/flowcli/vulnerability/__init__.py +3 -0
- convisoappsec/flowcli/vulnerability/assert_security_rules.py +201 -0
- convisoappsec/flowcli/vulnerability/container_vulnerability_manager.py +175 -0
- convisoappsec/flowcli/vulnerability/entrypoint.py +18 -0
- convisoappsec/flowcli/vulnerability/rules_schema.json +53 -0
- convisoappsec/flowcli/vulnerability/run.py +487 -0
- convisoappsec/logger.py +29 -0
- convisoappsec/sast/__init__.py +0 -0
- convisoappsec/sast/decision.py +45 -0
- convisoappsec/sast/sastbox.py +296 -0
- 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,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
|
+
)
|