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,399 @@
|
|
|
1
|
+
import tarfile
|
|
2
|
+
import tempfile
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from threading import Lock, Thread
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from convisoappsec.flow.util import SourceCodeCompressor
|
|
9
|
+
from convisoappsec.logger import LOGGER
|
|
10
|
+
|
|
11
|
+
import docker
|
|
12
|
+
|
|
13
|
+
from .exceptions import *
|
|
14
|
+
|
|
15
|
+
mutex = Lock()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EventLogging(Thread):
|
|
19
|
+
|
|
20
|
+
def __init__(self, logger, events):
|
|
21
|
+
super().__init__(name='Docker-Events', daemon=True)
|
|
22
|
+
self.logger = logger or LOGGER
|
|
23
|
+
self.events = events
|
|
24
|
+
|
|
25
|
+
def docker_log(self, event):
|
|
26
|
+
ts = datetime.fromtimestamp(event['time']).strftime('%d/%m/%Y %H:%M:%S')
|
|
27
|
+
message = "{timestamp}|{Type} {Action} on {Actor[ID]}".format(**event, timestamp=ts)
|
|
28
|
+
self.logger.debug(message)
|
|
29
|
+
|
|
30
|
+
def run(self):
|
|
31
|
+
for event in self.events:
|
|
32
|
+
if 'conviso-cli-' in event['Actor']['ID']:
|
|
33
|
+
self.docker_log(event)
|
|
34
|
+
self.logger.debug('Docker Events closed')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Credentials:
|
|
38
|
+
|
|
39
|
+
AUTHS = {}
|
|
40
|
+
|
|
41
|
+
def __init__(self, docker, logger):
|
|
42
|
+
self.docker = docker
|
|
43
|
+
self.logger = logger or LOGGER
|
|
44
|
+
|
|
45
|
+
def login(self, registry, password, username='AWS'):
|
|
46
|
+
mutex.acquire()
|
|
47
|
+
if not registry in Credentials.AUTHS.keys():
|
|
48
|
+
self.logger.debug(
|
|
49
|
+
" \U0001F511 Checking Authorization for {0}".format(registry))
|
|
50
|
+
login_args = {
|
|
51
|
+
'registry': registry,
|
|
52
|
+
'username': username,
|
|
53
|
+
'password': password,
|
|
54
|
+
'reauth': True,
|
|
55
|
+
}
|
|
56
|
+
try:
|
|
57
|
+
login_result = self.docker.login(**login_args)
|
|
58
|
+
status = login_result['Status']
|
|
59
|
+
self.logger.debug("Login result was {0}".format(login_result))
|
|
60
|
+
if status == 'Login Succeeded':
|
|
61
|
+
Credentials.AUTHS[registry] = password
|
|
62
|
+
self.logger.debug(' \U0001F513 ' + status)
|
|
63
|
+
except docker.errors.APIError:
|
|
64
|
+
self.logger.debug("Docker API Error")
|
|
65
|
+
mutex.release()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SCSCommon:
|
|
69
|
+
|
|
70
|
+
# General Settings
|
|
71
|
+
DEFAULT_REGISTRY = 'public.ecr.aws/convisoappsec'
|
|
72
|
+
DEFAULT_TAG = 'latest'
|
|
73
|
+
DEFAULT_CONTAINER_CODE_DIR = '/code'
|
|
74
|
+
SUCCESS_EXIT_CODE = 0
|
|
75
|
+
DOCKER_CLIENT = None
|
|
76
|
+
DOCKER_CREDENTIALS = None
|
|
77
|
+
DOCKER_EVENTS = None
|
|
78
|
+
DOCKER_EVENTS_THREAD = None
|
|
79
|
+
TEMPDIR = None
|
|
80
|
+
|
|
81
|
+
def __init__(self, tag=None, registry=None, repository_name=None, token=None, logger=None, command=None, repository_dir=None):
|
|
82
|
+
self.logger = logger or LOGGER
|
|
83
|
+
self.token = token
|
|
84
|
+
uuid = str(uuid4())
|
|
85
|
+
self.docker = self._get_docker_client()
|
|
86
|
+
self.__container_name = "conviso-cli-{0}".format(uuid)
|
|
87
|
+
self.__docker_events = self._get_docker_events()
|
|
88
|
+
self.__docker_events_thread = self._get_docker_events_thread()
|
|
89
|
+
self.__credentials = self._get_docker_credentials()
|
|
90
|
+
self.__source_code_volume_name = "conviso-cli-{0}".format(uuid)
|
|
91
|
+
|
|
92
|
+
self.container = None
|
|
93
|
+
self.tag = tag or self.DEFAULT_TAG
|
|
94
|
+
self.registry = registry or self.DEFAULT_REGISTRY
|
|
95
|
+
self.repository_name = repository_name
|
|
96
|
+
self.command = command
|
|
97
|
+
self.repository_dir = repository_dir
|
|
98
|
+
if token:
|
|
99
|
+
self._login(self.token)
|
|
100
|
+
|
|
101
|
+
def _get_docker_client(self):
|
|
102
|
+
mutex.acquire()
|
|
103
|
+
if not SCSCommon.DOCKER_CLIENT:
|
|
104
|
+
SCSCommon.DOCKER_CLIENT = docker.from_env(
|
|
105
|
+
version="auto"
|
|
106
|
+
)
|
|
107
|
+
mutex.release()
|
|
108
|
+
return SCSCommon.DOCKER_CLIENT
|
|
109
|
+
|
|
110
|
+
def _get_docker_credentials(self):
|
|
111
|
+
mutex.acquire()
|
|
112
|
+
if not SCSCommon.DOCKER_CREDENTIALS:
|
|
113
|
+
SCSCommon.DOCKER_CREDENTIALS = Credentials(
|
|
114
|
+
docker=self.docker,
|
|
115
|
+
logger=self.logger
|
|
116
|
+
)
|
|
117
|
+
mutex.release()
|
|
118
|
+
return SCSCommon.DOCKER_CREDENTIALS
|
|
119
|
+
|
|
120
|
+
def _get_docker_events(self):
|
|
121
|
+
mutex.acquire()
|
|
122
|
+
if not SCSCommon.DOCKER_EVENTS:
|
|
123
|
+
SCSCommon.DOCKER_EVENTS = self.docker.events(decode=True)
|
|
124
|
+
mutex.release()
|
|
125
|
+
return SCSCommon.DOCKER_EVENTS
|
|
126
|
+
|
|
127
|
+
def _get_docker_events_thread(self):
|
|
128
|
+
mutex.acquire()
|
|
129
|
+
if not SCSCommon.DOCKER_EVENTS_THREAD:
|
|
130
|
+
SCSCommon.DOCKER_EVENTS_THREAD = EventLogging(
|
|
131
|
+
logger=self.logger,
|
|
132
|
+
events=self.__docker_events,
|
|
133
|
+
)
|
|
134
|
+
SCSCommon.DOCKER_EVENTS_THREAD.start()
|
|
135
|
+
mutex.release()
|
|
136
|
+
return SCSCommon.DOCKER_EVENTS_THREAD
|
|
137
|
+
|
|
138
|
+
def _login(self, password, username='AWS'):
|
|
139
|
+
self.__credentials.login(
|
|
140
|
+
registry=self.registry,
|
|
141
|
+
password=password,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _create_host_output_directory(self):
|
|
145
|
+
"""Creates a directory on the host machine to store container outputs.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
string: absolute path to the directory
|
|
149
|
+
"""
|
|
150
|
+
if not SCSCommon.TEMPDIR:
|
|
151
|
+
SCSCommon.TEMPDIR = tempfile.mkdtemp(
|
|
152
|
+
prefix='conviso-output-',
|
|
153
|
+
dir=self.repository_dir
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
self.logger.debug("Created output directory at {}".format(
|
|
157
|
+
SCSCommon.TEMPDIR
|
|
158
|
+
))
|
|
159
|
+
return SCSCommon.TEMPDIR
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def size(self):
|
|
163
|
+
registry_data = self.docker.images.get_registry_data(self.image)
|
|
164
|
+
|
|
165
|
+
descriptor = registry_data.attrs.get('Descriptor', {})
|
|
166
|
+
return descriptor.get('size') * 1024 * 1024
|
|
167
|
+
|
|
168
|
+
def run(self):
|
|
169
|
+
container = self.__create_container()
|
|
170
|
+
self.__inject_container_source_code(container)
|
|
171
|
+
container.start()
|
|
172
|
+
|
|
173
|
+
container_stderr = container.logs(
|
|
174
|
+
stream=True,
|
|
175
|
+
stdout=False,
|
|
176
|
+
stderr=True
|
|
177
|
+
)
|
|
178
|
+
for message in container_stderr:
|
|
179
|
+
self.logger.debug(message)
|
|
180
|
+
|
|
181
|
+
container_stdout = container.logs(
|
|
182
|
+
stream=True,
|
|
183
|
+
stdout=True,
|
|
184
|
+
stderr=False,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
for message in container_stdout:
|
|
188
|
+
self.logger.debug(message)
|
|
189
|
+
|
|
190
|
+
def wait(self):
|
|
191
|
+
wait_result = self.__container.wait()
|
|
192
|
+
status_code = wait_result.get('StatusCode')
|
|
193
|
+
|
|
194
|
+
if not status_code == self.SUCCESS_EXIT_CODE:
|
|
195
|
+
raise CommonException()
|
|
196
|
+
|
|
197
|
+
return status_code
|
|
198
|
+
|
|
199
|
+
def __has_method(self, method_name):
|
|
200
|
+
return hasattr(self, method_name)
|
|
201
|
+
|
|
202
|
+
def pull(self):
|
|
203
|
+
'''
|
|
204
|
+
Acts as docker pull <image_name>, downloading the image configured in the instance.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
# Example:
|
|
208
|
+
{
|
|
209
|
+
'status': 'Downloading',
|
|
210
|
+
'progressDetail': {'current': int, 'total': int},
|
|
211
|
+
'id': 'string'
|
|
212
|
+
}
|
|
213
|
+
'''
|
|
214
|
+
self.logger.debug(
|
|
215
|
+
"Pulling scanner image {}. It might takes some minutes.".format(
|
|
216
|
+
self.repository_name)
|
|
217
|
+
)
|
|
218
|
+
if self.has_pre_pull:
|
|
219
|
+
self._pre_pull()
|
|
220
|
+
self.logger.debug("End of pre_pull method.")
|
|
221
|
+
try:
|
|
222
|
+
return self.docker.images.pull(
|
|
223
|
+
repository=self.repository,
|
|
224
|
+
tag=self.tag
|
|
225
|
+
)
|
|
226
|
+
except docker.errors.ImageNotFound:
|
|
227
|
+
self.logger.debug(
|
|
228
|
+
"Image {} not found on registry".format(self.image)
|
|
229
|
+
)
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
def __get_container(self):
|
|
233
|
+
return self.docker.containers.get(
|
|
234
|
+
self.__container_name
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def __container(self):
|
|
239
|
+
try:
|
|
240
|
+
return self.__get_container()
|
|
241
|
+
except:
|
|
242
|
+
return self.__create_container()
|
|
243
|
+
|
|
244
|
+
def __create_container(self):
|
|
245
|
+
self.logger.debug(
|
|
246
|
+
"Creating Container {0}".format(self.__container_name)
|
|
247
|
+
)
|
|
248
|
+
return self.docker.containers.create(
|
|
249
|
+
self.image,
|
|
250
|
+
name=self.__container_name,
|
|
251
|
+
volumes=self.volumes,
|
|
252
|
+
detach=True,
|
|
253
|
+
command=self.command
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def repository(self):
|
|
258
|
+
return "{registry}/{repository_name}".format(
|
|
259
|
+
registry=self.registry,
|
|
260
|
+
repository_name=self.repository_name,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def image(self):
|
|
265
|
+
return "{repository}:{tag}".format(
|
|
266
|
+
repository=self.repository,
|
|
267
|
+
tag=self.tag,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def has_pre_pull(self):
|
|
272
|
+
return self.__has_method('_pre_pull')
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def has_read_scan_stdout(self):
|
|
276
|
+
return self.__has_method('_read_scan_stdout')
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def has_read_scan_stderr(self):
|
|
280
|
+
return self.__has_method('_read_scan_stderr')
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def volumes(self):
|
|
284
|
+
return {
|
|
285
|
+
self.__source_code_volume_name: {
|
|
286
|
+
'bind': SCSCommon.DEFAULT_CONTAINER_CODE_DIR,
|
|
287
|
+
'mode': 'rw',
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def name(self):
|
|
293
|
+
return self.__container_name
|
|
294
|
+
|
|
295
|
+
def __del__(self):
|
|
296
|
+
self.logger.debug(
|
|
297
|
+
"Removing container {0}".format(self.__container_name)
|
|
298
|
+
)
|
|
299
|
+
with suppress(Exception):
|
|
300
|
+
self.container.remove(v=True, force=True)
|
|
301
|
+
|
|
302
|
+
def __inject_container_source_code(self, container):
|
|
303
|
+
try:
|
|
304
|
+
self.logger.debug(
|
|
305
|
+
"Loading Source code into {0}".format(self.__container_name)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
with tempfile.TemporaryFile() as wrapper_file:
|
|
309
|
+
compressor = SourceCodeCompressor(
|
|
310
|
+
self.repository_dir
|
|
311
|
+
)
|
|
312
|
+
compressor.write_to(wrapper_file)
|
|
313
|
+
wrapper_file.seek(0)
|
|
314
|
+
container.put_archive(
|
|
315
|
+
SCSCommon.DEFAULT_CONTAINER_CODE_DIR,
|
|
316
|
+
wrapper_file
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
self.logger.error(e)
|
|
321
|
+
|
|
322
|
+
def _get_container_tarball_chunks(self, container_filepath):
|
|
323
|
+
"""From the path provided you get the file binary/chunks from the container.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
container_filepath (string): Absolute Path of the file within the container
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
int: For further information see https://docker-py.readthedocs.io/en/stable/containers.html#docker.models.containers.Container.get_archive
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
container = self.__container
|
|
333
|
+
chunks, _ = container.get_archive(container_filepath)
|
|
334
|
+
return chunks
|
|
335
|
+
|
|
336
|
+
except docker.errors.NotFound:
|
|
337
|
+
self.logger.debug("{} does not detected issues, continuing...".format(
|
|
338
|
+
self.repository_name
|
|
339
|
+
))
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
def _extract_tarball_chunks(self, tarball_chunks, report_filename):
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
tarball_chunks (int): The number of bytes returned by each iteration of the generator
|
|
347
|
+
report_filename (string): The name of the extracted report
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
string: Report absolute filepath in local filesystem
|
|
351
|
+
"""
|
|
352
|
+
output_dirpath = self._create_host_output_directory()
|
|
353
|
+
report_absolute_filepath = '{}/{}'.format(
|
|
354
|
+
output_dirpath, report_filename
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
with tempfile.TemporaryFile() as tmp_wrapper_file:
|
|
358
|
+
for chunk in tarball_chunks:
|
|
359
|
+
tmp_wrapper_file.write(chunk)
|
|
360
|
+
tmp_wrapper_file.seek(0)
|
|
361
|
+
|
|
362
|
+
with tarfile.open(mode="r|", fileobj=tmp_wrapper_file) as talball_file:
|
|
363
|
+
talball_file.extractall(path=output_dirpath)
|
|
364
|
+
|
|
365
|
+
self.logger.debug("report from {} saved to {}".format(
|
|
366
|
+
self.__container_name,
|
|
367
|
+
report_absolute_filepath
|
|
368
|
+
))
|
|
369
|
+
return report_absolute_filepath
|
|
370
|
+
|
|
371
|
+
def get_container_reports(self):
|
|
372
|
+
""" Get all reports found in the instantiated container and save on the host machine.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
string: Report absolute filepath in local filesystem
|
|
376
|
+
"""
|
|
377
|
+
self.logger.debug(
|
|
378
|
+
"Collecting reports from {0}".format(self.__container_name)
|
|
379
|
+
)
|
|
380
|
+
try:
|
|
381
|
+
possible_report_extensions = ['sarif', 'json']
|
|
382
|
+
for ext in possible_report_extensions:
|
|
383
|
+
report_filename = '{}.{}'.format(self.repository_name, ext)
|
|
384
|
+
|
|
385
|
+
container_filepath = '/{}'.format(report_filename)
|
|
386
|
+
tarball_chunks = self._get_container_tarball_chunks(
|
|
387
|
+
container_filepath
|
|
388
|
+
)
|
|
389
|
+
if not tarball_chunks:
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
absolute_report_path = self._extract_tarball_chunks(
|
|
393
|
+
tarball_chunks,
|
|
394
|
+
report_filename
|
|
395
|
+
)
|
|
396
|
+
return absolute_report_path
|
|
397
|
+
|
|
398
|
+
except Exception as e:
|
|
399
|
+
self.logger.error(e)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
|
|
2
|
+
from git import Repo
|
|
3
|
+
from git.exc import GitCommandError, InvalidGitRepositoryError
|
|
4
|
+
from giturlparse import parse as gitparse
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from convisoappsec.flow.version_control_system_adapter import GitAdapter
|
|
9
|
+
from convisoappsec.logger import LOGGER
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DescriptionParsingError(GitCommandError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GitDataParser:
|
|
17
|
+
def __init__(self, repository_dir):
|
|
18
|
+
try:
|
|
19
|
+
self.__validate_git_repo(repository_dir)
|
|
20
|
+
|
|
21
|
+
self.repository_dir = repository_dir
|
|
22
|
+
self._git_adapter = GitAdapter(repository_dir)
|
|
23
|
+
|
|
24
|
+
except InvalidGitRepositoryError as exp:
|
|
25
|
+
LOGGER.error(
|
|
26
|
+
'Invalid Git repository: "{}".'.format(repository_dir)
|
|
27
|
+
)
|
|
28
|
+
raise exp
|
|
29
|
+
|
|
30
|
+
except Exception as exp:
|
|
31
|
+
LOGGER.error("An unxpected error occurred.")
|
|
32
|
+
raise exp
|
|
33
|
+
|
|
34
|
+
def parse_name(self):
|
|
35
|
+
try:
|
|
36
|
+
git_remote_url = self._git_adapter._git_client.ls_remote(
|
|
37
|
+
'--get-url'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return self.__parse_name_from_git_url(git_remote_url)
|
|
41
|
+
except GitCommandError:
|
|
42
|
+
dirname = os.path.dirname(self.repository_dir)
|
|
43
|
+
basename = os.path.basename(self.repository_dir)
|
|
44
|
+
digest = sha256(dirname.encode('utf-8')).hexdigest()
|
|
45
|
+
|
|
46
|
+
return basename + '-' + digest
|
|
47
|
+
except Exception as exception:
|
|
48
|
+
raise exception
|
|
49
|
+
|
|
50
|
+
def __parse_description(self):
|
|
51
|
+
try:
|
|
52
|
+
readme = self._git_adapter._git_client.show(':README.md')
|
|
53
|
+
readme = readme.replace('\n', '\n<br>')
|
|
54
|
+
|
|
55
|
+
return readme
|
|
56
|
+
|
|
57
|
+
except DescriptionParsingError as exception:
|
|
58
|
+
raise exception
|
|
59
|
+
|
|
60
|
+
except Exception as exception:
|
|
61
|
+
raise exception
|
|
62
|
+
|
|
63
|
+
def __parse_name_from_git_url(self, git_url):
|
|
64
|
+
parser = gitparse(git_url)
|
|
65
|
+
|
|
66
|
+
return '{} ({}/{})'.format(
|
|
67
|
+
parser.name,
|
|
68
|
+
parser.resource,
|
|
69
|
+
parser.owner,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def __validate_git_repo(self, repository_dir):
|
|
73
|
+
repo = Repo(repository_dir)
|
|
74
|
+
repo.git.config("--global", "--add", "safe.directory", repository_dir)
|
|
75
|
+
|
|
76
|
+
return repo
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import jmespath
|
|
3
|
+
|
|
4
|
+
from convisoappsec.common.graphql.errors import AuthenticationError, Error, ResponseError, ServerError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RequestErrorHandler:
|
|
8
|
+
def __init__(self, error):
|
|
9
|
+
self.error = error
|
|
10
|
+
|
|
11
|
+
def handle_request_error(self):
|
|
12
|
+
try:
|
|
13
|
+
raise self.error
|
|
14
|
+
except requests.exceptions.HTTPError as e:
|
|
15
|
+
self._handle_http_error(e)
|
|
16
|
+
except Exception as e:
|
|
17
|
+
self._handle_unexpected_error(e)
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def _handle_unexpected_error(error):
|
|
21
|
+
raise Error(str(error)) from error
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _handle_http_error(error):
|
|
25
|
+
if error.response.status_code == 401:
|
|
26
|
+
error_msg_fmt = 'Unauthorized access, check your credentials. {}'
|
|
27
|
+
response = error.response.json()
|
|
28
|
+
error_messages = response.get('errors', [])
|
|
29
|
+
error_msg = error_msg_fmt.format(error_messages)
|
|
30
|
+
|
|
31
|
+
raise AuthenticationError(error_msg) from error
|
|
32
|
+
|
|
33
|
+
if error.response.status_code == 500:
|
|
34
|
+
error_msg_fmt = 'Internal Server Error. {}'
|
|
35
|
+
error_msg = error_msg_fmt.format(error.response.text)
|
|
36
|
+
|
|
37
|
+
raise ServerError(error_msg) from error
|
|
38
|
+
|
|
39
|
+
raise Error(error.response.text) from error
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GraphQlErrorHandler:
|
|
43
|
+
def __init__(self, response):
|
|
44
|
+
self.response = response
|
|
45
|
+
|
|
46
|
+
def raise_on_graphql_body_error(self):
|
|
47
|
+
data = self.response.get('data', [])
|
|
48
|
+
|
|
49
|
+
for key in data:
|
|
50
|
+
if data[key] is None:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
errors = data[key].get('errors', [])
|
|
54
|
+
has_errors = len(errors) > 0
|
|
55
|
+
if has_errors:
|
|
56
|
+
raise ResponseError(errors)
|
|
57
|
+
|
|
58
|
+
def raise_on_graphql_error(self):
|
|
59
|
+
errors = self.response.get('errors', [])
|
|
60
|
+
|
|
61
|
+
if not errors:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
error = errors[0]
|
|
65
|
+
|
|
66
|
+
error_path = 'extensions.code'
|
|
67
|
+
|
|
68
|
+
code = jmespath.search(
|
|
69
|
+
error_path,
|
|
70
|
+
error
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
message = error.get('message', '')
|
|
74
|
+
|
|
75
|
+
raise ResponseError(message, code)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from convisoappsec.common.graphql.error_handlers import GraphQlErrorHandler, RequestErrorHandler
|
|
4
|
+
from convisoappsec.version import __version__
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GraphQLClient:
|
|
8
|
+
DEFAULT_HEADERS = {
|
|
9
|
+
'Accept': 'application/json',
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
"User-Agent": "AST:{version}".format(version=__version__),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def __init__(self, url, headers={}):
|
|
15
|
+
self.url = url
|
|
16
|
+
self.__session = requests.Session()
|
|
17
|
+
self.__session.headers.update(
|
|
18
|
+
**self.DEFAULT_HEADERS,
|
|
19
|
+
**headers
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def execute(self, query, variables={}):
|
|
23
|
+
try:
|
|
24
|
+
payload = self._build_graphql_payload(query, variables)
|
|
25
|
+
|
|
26
|
+
response = self.__session.post(
|
|
27
|
+
url=self.url,
|
|
28
|
+
json=payload,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
response.raise_for_status()
|
|
32
|
+
|
|
33
|
+
except Exception as e:
|
|
34
|
+
handler = RequestErrorHandler(e)
|
|
35
|
+
handler.handle_request_error()
|
|
36
|
+
|
|
37
|
+
json_response = response.json()
|
|
38
|
+
graphql_handler = GraphQlErrorHandler(json_response)
|
|
39
|
+
graphql_handler.raise_on_graphql_error()
|
|
40
|
+
graphql_handler.raise_on_graphql_body_error()
|
|
41
|
+
|
|
42
|
+
return json_response['data']
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _build_graphql_payload(query, variables):
|
|
46
|
+
data = {
|
|
47
|
+
'query': query,
|
|
48
|
+
'variables': variables
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return data
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import traceback
|
|
3
|
+
from convisoappsec.logger import LOGGER, log_and_notify_ast_event
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RetryHandler:
|
|
7
|
+
def __init__(self, flow_context=None, company_id=None, asset_id=None):
|
|
8
|
+
self.max_retries = 5
|
|
9
|
+
self.initial_delay = 1
|
|
10
|
+
self.backoff_factor = 2
|
|
11
|
+
self.flow_context = flow_context
|
|
12
|
+
self.company_id = company_id
|
|
13
|
+
self.asset_id = asset_id
|
|
14
|
+
|
|
15
|
+
def execute_with_retry(self, func, *args, **kwargs):
|
|
16
|
+
"""Execute a function with retry logic and exponential backoff."""
|
|
17
|
+
retries = 0
|
|
18
|
+
delay = self.initial_delay
|
|
19
|
+
|
|
20
|
+
while retries < self.max_retries:
|
|
21
|
+
try:
|
|
22
|
+
return func(*args, **kwargs)
|
|
23
|
+
except Exception:
|
|
24
|
+
retries += 1
|
|
25
|
+
time.sleep(delay)
|
|
26
|
+
delay *= self.backoff_factor
|
|
27
|
+
|
|
28
|
+
if retries == self.max_retries:
|
|
29
|
+
full_trace = traceback.format_exc()
|
|
30
|
+
LOGGER.warning(
|
|
31
|
+
"⚠️ Maximum retries reached. Our technical team has been notified."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
log_and_notify_ast_event(
|
|
36
|
+
flow_context=self.flow_context, company_id=self.company_id, asset_id=self.asset_id,
|
|
37
|
+
ast_log=full_trace
|
|
38
|
+
)
|
|
39
|
+
except Exception as log_error:
|
|
40
|
+
LOGGER.warning(f"⚠️ Failed to log and notify AST event: {log_error}")
|