polyswarm-engine 3.1.1__py2.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.
@@ -0,0 +1,316 @@
1
+ import os
2
+ import builtins
3
+ import datetime as dt
4
+ import functools
5
+ import inspect
6
+ import importlib
7
+ import json
8
+ import logging
9
+ import pathlib
10
+ import typing as t
11
+
12
+ import click
13
+
14
+ import polyswarm_engine.settings
15
+ from polyswarm_engine.bounty import forge_local_bounty
16
+ from polyswarm_engine.constants import (
17
+ ARTIFACT_TYPES,
18
+ BENIGN,
19
+ EICAR_CONTENT,
20
+ FILE_ARTIFACT,
21
+ MALICIOUS,
22
+ UNKNOWN,
23
+ URL_ARTIFACT,
24
+ )
25
+ from polyswarm_engine.utils import is_fifo
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Artifact type for manually constructed bounties
30
+ BOUNTY_ARTIFACT = "bounty"
31
+
32
+
33
+ @click.group()
34
+ def engine_cli():
35
+ from logging.config import dictConfig
36
+
37
+ from polyswarm_engine import log_config
38
+
39
+ dictConfig(log_config.get_logging(handler='click'))
40
+
41
+
42
+ @engine_cli.command("devserver")
43
+ @click.option('--port', '-p', help='Server port', type=int, default=8000, show_default=True)
44
+ @click.option('--secret', '-s', help='Webhook secret [env: PSENGINE_WEBHOOK_SECRET]', envvar='PSENGINE_WEBHOOK_SECRET')
45
+ @click.pass_obj
46
+ def devserver(engine, port, secret, **kwargs):
47
+ """
48
+ Simple HTTP server only usable during development
49
+ """
50
+ from logging.config import dictConfig
51
+ from wsgiref.simple_server import make_server
52
+
53
+ from polyswarm_engine import log_config
54
+ from polyswarm_engine.wsgi import ValidateSenderMiddleware, application as wsgi_app
55
+
56
+ dictConfig(log_config.get_logging())
57
+
58
+ wsgi_app = ValidateSenderMiddleware(wsgi_app, secret=secret)
59
+ with make_server('', port, wsgi_app) as httpd:
60
+ print("Serving {} on port {}, control-C to stop".format(engine.name, port))
61
+ httpd.serve_forever()
62
+
63
+
64
+ @engine_cli.command('create-vhost', context_settings=dict(show_default=True))
65
+ @click.option('--vhost', envvar='PSENGINE_BROKER_VHOST', default='engines')
66
+ @click.option('--broker', envvar='PSENGINE_BROKER_URL', default='amqp://user:password@rabbitmq:5672')
67
+ def create_vhost(vhost: str, broker: str):
68
+ """Ensure that a vhost exists in the RabbitMQ broker"""
69
+ from urllib import parse
70
+ import requests
71
+ from polyswarm_engine.celeryconfig import CeleryConfig
72
+
73
+ broker_url = CeleryConfig(broker=broker, vhost=vhost).broker_url
74
+ parsed_url = parse.urlparse(broker_url)
75
+ vhost = vhost or parsed_url.path.strip(' /')
76
+
77
+ if vhost:
78
+ logger.info("Creating '%s' vhost", vhost)
79
+ create_url = parse.urlunparse(
80
+ ('http', f'{parsed_url.hostname}:1{parsed_url.port}', f'/api/vhosts/{vhost}', '', '', '')
81
+ )
82
+ r = requests.put(create_url, auth=(parsed_url.username, parsed_url.password))
83
+ r.raise_for_status()
84
+ click.echo(f'Successfully create vhost {vhost}')
85
+ else:
86
+ click.echo('No vhost defined for the celery broker')
87
+
88
+
89
+ def _gather_analyses(backend, artifacts, artifact_type):
90
+ futures = list()
91
+ for artifact in artifacts:
92
+ bounty = _make_bounty(artifact, artifact_type)
93
+ analysis = backend.analyze(bounty)
94
+ result = (artifact, bounty, analysis)
95
+
96
+ # If our result is already ready, print it immediately
97
+ if analysis.ready():
98
+ yield result
99
+ else:
100
+ futures.append(result)
101
+
102
+ yield from futures
103
+
104
+
105
+ @engine_cli.command("analyze", help="Analyze artifacts")
106
+ @click.option("-v", "--verbose", count=True)
107
+ @click.option("--check-empty", help="Verify this engine can analyze an empty bounty", default=False, is_flag=True)
108
+ @click.option("--check-eicar", help="Verify this engine can analyze EICAR test file", default=False, is_flag=True)
109
+ @click.option(
110
+ '--check-wicar',
111
+ '--check-exploit-url',
112
+ help='Verify this engine can analyze the WICAR exploit kit URL',
113
+ default=False,
114
+ is_flag=True,
115
+ )
116
+ @click.option(
117
+ "--artifact-type",
118
+ "-t",
119
+ type=click.Choice([BOUNTY_ARTIFACT, *ARTIFACT_TYPES], case_sensitive=False),
120
+ default=FILE_ARTIFACT,
121
+ help="Artifact type to use when constructing bounties. "
122
+ f"'{BOUNTY_ARTIFACT}' loads manually constructed bounties, "
123
+ "treating each argument as the path to a JSON-encoded bounty object"
124
+ )
125
+ @click.argument("artifacts", nargs=-1)
126
+ @click.pass_obj
127
+ def analyze(engine, artifacts, artifact_type, verbose, check_eicar, check_empty, check_wicar, **kwargs):
128
+ # force celery backend to be eager when running local analyze
129
+ os.environ['PSENGINE_TASK_ALWAYS_EAGER'] = '1'
130
+ importlib.reload(polyswarm_engine.settings)
131
+
132
+ with engine.create_backend() as backend:
133
+ if check_eicar:
134
+ analysis = backend.analyze(_make_bounty(EICAR_CONTENT, FILE_ARTIFACT)).get()
135
+ _check_analysis(analysis, expected={MALICIOUS})
136
+
137
+ if check_empty:
138
+ analysis = backend.analyze(_make_bounty(b'', FILE_ARTIFACT)).get()
139
+ _check_analysis(analysis, expected={BENIGN, UNKNOWN})
140
+
141
+ if check_wicar:
142
+ # MS05-054 Microsoft Internet Explorer JavaScript OnLoad Handler
143
+ url = "http://malware.wicar.org/data/ms05_054_onload.html"
144
+ analysis = backend.analyze(_make_bounty(url, URL_ARTIFACT)).get()
145
+ _check_analysis(analysis, expected={MALICIOUS})
146
+
147
+ for artifact, bounty, future in _gather_analyses(backend, artifacts, artifact_type):
148
+ if artifact and len(artifacts) > 1:
149
+ _echo(f"{artifact:-^80}", ostream="stderr")
150
+
151
+ if verbose:
152
+ _echo("Bounty: ", nl=False, ostream="stderr")
153
+ _echo(bounty, bold=True, ostream="stderr")
154
+ _echo("Analysis: ", nl=False, ostream="stderr")
155
+
156
+ _echo(future.get(), bold=bool(verbose))
157
+
158
+
159
+ def _check_analysis(analysis, expected):
160
+ _echo(analysis)
161
+ assert isinstance(analysis, t.Mapping)
162
+ assert analysis["verdict"] in expected, f"Received '{analysis['verdict']}' instead of {' or '.join(expected)}"
163
+
164
+
165
+ def _make_bounty(artifact, artifact_type, **kwargs):
166
+ forge = functools.partial(forge_local_bounty, artifact_type=artifact_type, **kwargs)
167
+
168
+ if artifact_type == BOUNTY_ARTIFACT:
169
+ return json.load(click.open_file(artifact, "rb"))
170
+ elif isinstance(artifact, bytes):
171
+ return forge(data=artifact)
172
+ elif artifact == "-" or is_fifo(artifact):
173
+ return forge(stream=click.open_file(artifact, "rb"))
174
+ elif artifact_type == URL_ARTIFACT:
175
+ return forge(data=artifact)
176
+ elif artifact_type == FILE_ARTIFACT:
177
+ return forge(path=artifact)
178
+ else:
179
+ raise ValueError(f"Invalid artifact: {artifact}")
180
+
181
+
182
+ @engine_cli.command("create-bounty", help="Make a fresh bounty from a file or URL artifact")
183
+ @click.option(
184
+ "--artifact-type",
185
+ "-t",
186
+ type=click.Choice(list(ARTIFACT_TYPES), case_sensitive=False),
187
+ default=FILE_ARTIFACT,
188
+ help="Artifact type to use when constructing bounties. "
189
+ )
190
+ @click.option("--expiration", type=int, default=60 * 60 * 24 * 365, help="Number of seconds until bounty expiration")
191
+ @click.option("--response-url", help="The URL to send results to")
192
+ @click.argument("artifact")
193
+ def create_bounty(artifact, artifact_type, expiration, response_url):
194
+ bounty = _make_bounty(artifact, artifact_type, expiration=dt.timedelta(seconds=expiration))
195
+
196
+ if response_url:
197
+ bounty["response_url"] = response_url
198
+
199
+ _echo(bounty)
200
+
201
+
202
+ @engine_cli.command(
203
+ "worker",
204
+ help="Start celery worker",
205
+ context_settings=dict(ignore_unknown_options=True),
206
+ )
207
+ @click.argument("celery_args", nargs=-1)
208
+ @click.pass_obj
209
+ def worker(engine, celery_args, **kwargs):
210
+ from logging.config import dictConfig
211
+
212
+ from polyswarm_engine import log_config
213
+
214
+ dictConfig(log_config.get_logging())
215
+
216
+ with engine.create_backend() as backend:
217
+ backend.app.worker_main(argv=["worker", *celery_args])
218
+
219
+
220
+ class EngineCommandsGroup(click.MultiCommand):
221
+ def list_commands(self, ctx):
222
+ return ctx.obj.cmd
223
+
224
+ def get_command(self, ctx, name):
225
+ engine = ctx.obj
226
+ cmd = engine.cmd[name]
227
+ func = cmd["func"]
228
+ argspec = inspect.getfullargspec(func)
229
+ docstr = cmd["doc"]
230
+
231
+ def callback(**params):
232
+ args = []
233
+
234
+ for arg in set(argspec.args) & set(params.keys()):
235
+ args.append(params.pop(arg))
236
+
237
+ if argspec.varargs in params:
238
+ args.extend(params.pop(argspec.varargs))
239
+
240
+ with engine.create_backend():
241
+ result = func(*args, **params)
242
+ _echo(result, fg="black", bold=True)
243
+
244
+ return click.Command(
245
+ name=name,
246
+ callback=callback,
247
+ help=docstr or name,
248
+ short_help=docstr.split("\n")[0] or None,
249
+ params=list(self._argspec_to_params(argspec)),
250
+ )
251
+
252
+ @staticmethod
253
+ def _argspec_to_params(spec: "inspect.FullArgSpec") -> "t.Iterator[click.Parameter]":
254
+ """Convert the function signature of a command to `click.Parameter` objects"""
255
+
256
+ def get_type(param_name):
257
+ if not spec.annotations:
258
+ return None
259
+
260
+ typ = spec.annotations.get(param_name)
261
+
262
+ if isinstance(typ, str):
263
+ if hasattr(builtins, typ):
264
+ return getattr(builtins, typ)
265
+ elif typ == "Path" or typ == "pathlib.Path":
266
+ return pathlib.Path
267
+ else:
268
+ return None
269
+
270
+ return typ
271
+
272
+ # `kwonlyargs` is a list of keyword-only parameter names in declaration order
273
+ if spec.kwonlyargs:
274
+ for name in spec.kwonlyargs:
275
+ # `kwonlydefaults` holds dictionary mapping parameter names from `kwonlyargs`
276
+ # to the default values used if no argument is supplied
277
+ if spec.kwonlydefaults and name in spec.kwonlydefaults:
278
+ yield click.Option([f"--{name}"], default=spec.kwonlydefaults[name], type=get_type(name))
279
+ else:
280
+ yield click.Option([f"--{name}"], required=True, type=get_type(name))
281
+
282
+ # `args` is a list of the positional parameter names
283
+ if spec.args:
284
+ # `defaults` is an n-tuple of default argument values for the last n positional parameters
285
+ if spec.defaults:
286
+ index = len(spec.args) - len(spec.defaults)
287
+
288
+ # yield each of the positional args w/o any associated defaults
289
+ for name in spec.args[:index]:
290
+ yield click.Argument([name], required=True, type=get_type(name))
291
+
292
+ # ... and then the rest of the positional args w/ default values
293
+ for name, default in zip(spec.args[index:], spec.defaults):
294
+ yield click.Argument([name], default=default, type=get_type(name))
295
+ else:
296
+ for name in spec.args:
297
+ yield click.Argument([name], required=True, type=get_type(name))
298
+
299
+ # `varargs` is the name of the * parameter or `None` if not accepted.
300
+ if spec.varargs:
301
+ yield click.Argument([spec.varargs], nargs=-1)
302
+
303
+
304
+ engine_cli.add_command(EngineCommandsGroup(name="commands", help="Engine commands"))
305
+
306
+
307
+ def _echo(msg, **echo_options):
308
+ if msg is None:
309
+ return
310
+ elif isinstance(msg, dict):
311
+ msg = json.dumps(msg, indent=2)
312
+
313
+ if "ostream" in echo_options:
314
+ echo_options["file"] = click.get_text_stream(echo_options.pop("ostream"))
315
+
316
+ click.secho(msg, **echo_options)
@@ -0,0 +1,34 @@
1
+ import inspect
2
+
3
+ from .utils import get_func_name, get_func_qual
4
+
5
+
6
+ class CommandRegistry:
7
+
8
+ def __init__(self):
9
+ self._metadata = dict()
10
+
11
+ def __iter__(self):
12
+ return iter(self._metadata)
13
+
14
+ def __getitem__(self, name):
15
+ return {"func": self.__dict__[name], **self._metadata[name]}
16
+
17
+ def __getattr__(self, name):
18
+ # XXX: This is here to make sure that `mypy` doesn't raise an error for items inside `self.__dict__`
19
+ ...
20
+
21
+ def _add(self, func, name=None):
22
+ module, fnname = get_func_qual(func)
23
+ name = name or fnname
24
+
25
+ if name.startswith('_'):
26
+ raise ValueError("Cannot use a command name starting with '_'")
27
+
28
+ self._metadata[name] = {
29
+ "name": name,
30
+ "qualname": get_func_name(func),
31
+ "module": module,
32
+ "doc": inspect.getdoc(func) or "",
33
+ }
34
+ self.__dict__[name] = func
@@ -0,0 +1,37 @@
1
+ import typing as t
2
+ import uuid
3
+ import base64
4
+
5
+ from .typing import AnalysisResult, ArtifactType
6
+
7
+ URL_MIMETYPE = "text/uri-list"
8
+
9
+ FILE_ARTIFACT: "ArtifactType" = "FILE"
10
+ URL_ARTIFACT: "ArtifactType" = "URL"
11
+ ARTIFACT_TYPES: "t.Set[ArtifactType]" = set(t.get_args(ArtifactType))
12
+
13
+ # Analysis Conclusions
14
+ BENIGN: "AnalysisResult" = "benign"
15
+ MALICIOUS: "AnalysisResult" = "malicious"
16
+ SUSPICIOUS: "AnalysisResult" = "suspicious"
17
+ UNKNOWN: "AnalysisResult" = "unknown"
18
+ AnalysisConclusions: "t.Set[AnalysisResult]" = set(t.get_args(AnalysisResult))
19
+
20
+ # These defined UUIDv5 namespaces are necessary to support the goal of semantic equivalence of some bounty objects.
21
+ # See: ``polyswarm_engine.bounty._forge_bounty_uuid``
22
+ BOUNTY_UUID = uuid.UUID("fafee1eb-ee7d-4b31-bee5-1547bd26c731")
23
+ FILE_BOUNTY_UUID = uuid.uuid5(BOUNTY_UUID, FILE_ARTIFACT)
24
+ URL_BOUNTY_UUID = uuid.uuid5(BOUNTY_UUID, URL_ARTIFACT)
25
+
26
+ SKIPPED_COMMENT = "SKIPPED"
27
+ SKIPPED_ENCRYPTED_COMMENT = f"{SKIPPED_COMMENT}:ENCRYPTED"
28
+ SKIPPED_HIGHCOMPRESSION_COMMENT = f"{SKIPPED_COMMENT}:DECOMPRESSION-UNSAFE"
29
+ SKIPPED_UNSUPPORTED_COMMENT = f"{SKIPPED_COMMENT}:TYPE-UNSUPPORTED"
30
+ SKIPPED_CANNOT_FETCH_COMMENT = f'{SKIPPED_COMMENT}:CANNOT-FETCH'
31
+
32
+ EICAR_CONTENT = base64.b64decode(
33
+ b'WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo='
34
+ )
35
+
36
+ # For easing the bid maths
37
+ NCT_TO_WEI_CONVERSION = 10**18
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+ import contextlib
3
+ import logging
4
+
5
+ from polyswarm_engine.backend import CeleryBackend
6
+ from polyswarm_engine.cli import engine_cli
7
+ from polyswarm_engine.command import CommandRegistry
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class EngineManager:
13
+ def __init__(self, name, vendor=None, config=None, backend_kwargs: dict = None, **kwargs):
14
+ self.name: str = name
15
+ self.vendor: str = vendor
16
+ self.config = config or dict()
17
+ self.ctx = dict()
18
+ self.backend: CeleryBackend|None = None
19
+ self.backend_kwargs = dict()
20
+ self.cmd = CommandRegistry()
21
+ # in case a lifecycle is not defined, use a nop context manager
22
+ self._lifecycle = lambda: contextlib.nullcontext()
23
+ self._head = None
24
+ self._analyze = None
25
+
26
+ def cli(self):
27
+ engine_cli(prog_name=self.name, obj=self)
28
+
29
+ @contextlib.contextmanager
30
+ def create_backend(self):
31
+ """
32
+ Start with backend
33
+
34
+ Example
35
+ -------
36
+
37
+ >>> with Engine.create_backend() as backend:
38
+ >>> ...
39
+ """
40
+ self.backend = CeleryBackend(
41
+ self.name,
42
+ self._analyze,
43
+ self._head,
44
+ self._lifecycle,
45
+ **self.backend_kwargs,
46
+ )
47
+ with self.backend.run() as backend:
48
+ yield backend
49
+ self.backend = None
50
+
51
+ def expose_command(self, func: "EngineCommandCallable"):
52
+ """Decorate to expose an internal engine function"""
53
+ self.cmd._add(func)
54
+ return func
55
+
56
+ def register_analyzer(self, func: "EngineAnalyzeCallable"):
57
+ """Decorator used to register this engine's analyzer function
58
+
59
+ Example::
60
+
61
+ @engine.register_analyzer
62
+ def analyze(bounty: polyswarm_engine.Bounty) -> polyswarm_engine.Analysis:
63
+ result = engine.cmd.scan_stream(get_artifact_stream(bounty))
64
+
65
+ analysis = {"verdict": polyswarm_engine.UNKNOWN}
66
+
67
+ if result["is_malicious"]:
68
+ analysis["verdict"] = polyswarm_engine.MALICIOUS
69
+
70
+ if "result_name" in result:
71
+ analysis["metadata"] = {"malware_family": result["result_name"]}
72
+
73
+ return analysis
74
+ """
75
+ self._analyze = func
76
+ return func
77
+
78
+ def register_head(self, func: "EngineHeadCallable"):
79
+ """Decorator used to gather engine metadata at startup
80
+
81
+ Notes::
82
+
83
+ This should decorate a function that gathers any data you'd
84
+ like to include with your analyses, but which isn't produced
85
+ as part of the scanning process such as:
86
+
87
+ - Engine version
88
+ - Signature version
89
+ - Current environment
90
+
91
+ Example::
92
+
93
+ @engine.register_head
94
+ def head() -> polyswarm_engine.AnalysisMetadata:
95
+ info = engine.cmd.info()
96
+
97
+ return {
98
+ "product": info["productName"],
99
+ "scanner": {
100
+ "vendor_version": info["productVersion"],
101
+ "signatures_version": info["vbaseVersion"],
102
+ }
103
+ }
104
+ """
105
+ self._head = func
106
+ return func
107
+
108
+ def register_lifecycle_manager(self, func: "EngineLifecycleCallable"):
109
+ """Wraps a function acting as a engine lifecycle ContextManager
110
+
111
+ Example::
112
+
113
+ @engine.register_lifecycle_manager
114
+ def lifecycle(Engine):
115
+ pid = run([Engine.config["DAEMON"], "--start"]) # Setup worker
116
+ yield
117
+ terminate(pid) # Worker has terminated, run cleanup code...
118
+ """
119
+ self._lifecycle = contextlib.contextmanager(func)
120
+ return func
121
+
122
+
123
+ __all__ = ["EngineManager"]
@@ -0,0 +1,41 @@
1
+ class EngineException(Exception):
2
+ pass
3
+
4
+
5
+ class EngineTimeoutError(EngineException):
6
+ pass
7
+
8
+
9
+ class EngineFileNotFoundError(EngineException):
10
+ pass
11
+
12
+
13
+ class EngineWorkerInterrupt(EngineException):
14
+ """ An exception that is not KeyboardInterrupt to allow subprocesses
15
+ to be interrupted.
16
+ """
17
+
18
+ pass
19
+
20
+
21
+ class EnginePollingException(EngineException):
22
+ """Base exception that stores all return values of attempted polls"""
23
+
24
+ def __init__(self, last=None):
25
+ self.last = last
26
+
27
+
28
+ class EngineExpiredException(EnginePollingException):
29
+ """Exception raised if polling function times out"""
30
+
31
+
32
+ class EngineMaxCallException(EnginePollingException):
33
+ """Exception raised if maximum number of iterations is exceeded"""
34
+
35
+
36
+ class BountyException(Exception):
37
+ """Bounty had problems"""
38
+
39
+
40
+ class BountyFetchException(Exception):
41
+ """Bounty artifact could not be fetched"""
@@ -0,0 +1,104 @@
1
+ from datetime import datetime, timezone as tz
2
+ import logging
3
+
4
+ from polyswarm_engine.settings import LOG_LEVEL, LOG_FORMAT
5
+
6
+ try:
7
+ from pythonjsonlogger import jsonlogger
8
+ except ImportError:
9
+ jsonlogger = None
10
+ else:
11
+
12
+ class JSONFormatter(jsonlogger.JsonFormatter):
13
+ """
14
+ Class to add custom JSON fields to our logger.
15
+ Presently just adds a timestamp if one isn't present and the log level.
16
+ INFO: https://github.com/madzak/python-json-logger#customizing-fields
17
+ """
18
+
19
+ def add_fields(self, log_record, record, message_dict):
20
+ super(JSONFormatter, self).add_fields(log_record, record, message_dict)
21
+ if not log_record.get('timestamp'):
22
+ # this doesn't use record.created, so it is slightly off
23
+ now = datetime.now(tz.utc).strftime('%Y-%m-%dT%H:%M:%S.%fZ')
24
+ log_record['timestamp'] = now
25
+ if log_record.get('level'):
26
+ log_record['level'] = log_record['level'].upper()
27
+ else:
28
+ log_record['level'] = record.levelname
29
+
30
+
31
+ try:
32
+ import click
33
+ import click_log
34
+ except ImportError:
35
+ click_log = None
36
+ else:
37
+ # adding color to INFO log messages as well
38
+ click_log.core.ColorFormatter.colors['info'] = dict(fg='green')
39
+
40
+ class NamedColorFormatter(logging.Formatter):
41
+ colors = {
42
+ 'error': dict(fg='red'),
43
+ 'exception': dict(fg='red'),
44
+ 'critical': dict(fg='red'),
45
+ 'debug': dict(fg='blue'),
46
+ 'warning': dict(fg='yellow'),
47
+ 'info': dict(fg='green'),
48
+ }
49
+
50
+ def format(self, record):
51
+ if not record.exc_info:
52
+ level = record.levelname.lower()
53
+ msg = logging.Formatter.format(self, record)
54
+ if level in self.colors:
55
+ sopts = self.colors[level]
56
+ lines = msg.splitlines()
57
+ msg = '\n'.join(click.style(x, **sopts) for x in lines) # type: ignore
58
+ return msg
59
+ return logging.Formatter.format(self, record)
60
+
61
+
62
+ def get_logging(log_level=None, handler='console'):
63
+ log_level = log_level or LOG_LEVEL
64
+ return {
65
+ 'version': 1,
66
+ 'disable_existing_loggers': False,
67
+ 'formatters': {
68
+ 'text': {
69
+ 'format': '%(asctime)s - %(levelname)-2s [%(filename)s:%(lineno)d][%(funcName)1s] %(message)s',
70
+ },
71
+ 'json': {
72
+ 'format': '%(asctime)s %(levelname) %(message) %(filename) %(lineno) %(funcName)',
73
+ 'class': 'polyswarm_engine.log_config.JSONFormatter',
74
+ },
75
+ 'click': {
76
+ 'format': '%(asctime)s - %(levelname)-2s [%(filename)s:%(lineno)d][%(funcName)1s] %(message)s',
77
+ 'class': 'polyswarm_engine.log_config.NamedColorFormatter',
78
+ },
79
+ },
80
+ 'handlers': {
81
+ 'console': {
82
+ 'level': log_level,
83
+ 'class': 'logging.StreamHandler',
84
+ 'formatter': LOG_FORMAT,
85
+ },
86
+ 'click': {
87
+ 'level': log_level,
88
+ 'class': 'click_log.core.ClickHandler',
89
+ 'formatter': 'click',
90
+ },
91
+ },
92
+ 'loggers': {
93
+ 'polyswarm_engine': {
94
+ 'level': log_level,
95
+ },
96
+ 'celery': {
97
+ 'level': log_level,
98
+ },
99
+ },
100
+ 'root': {
101
+ 'handlers': [handler],
102
+ 'level': log_level,
103
+ }
104
+ }
File without changes