frida-fusion 0.1.5__tar.gz → 0.1.7__tar.gz
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.
Potentially problematic release.
This version of frida-fusion might be problematic. Click here for more details.
- frida_fusion-0.1.7/LICENSE +25 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/PKG-INFO +8 -3
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/__meta__.py +2 -2
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/config.py +4 -2
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/fusion.py +76 -131
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/libs/color.py +0 -15
- frida_fusion-0.1.7/frida_fusion/libs/logger.py +169 -0
- frida_fusion-0.1.7/frida_fusion/libs/scriptlocation.py +39 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/modules/crypto/crypto.py +27 -6
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/modules/tls_unpinning/frida_multiple_unpinning.py +3 -8
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion.egg-info/PKG-INFO +8 -3
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion.egg-info/SOURCES.txt +2 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/pyproject.toml +2 -3
- frida-fusion-0.1.5/frida_fusion/libs/logger.py +0 -36
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/README.md +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/__init__.py +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/__main__.py +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/args.py +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/libs/__init__.py +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/libs/database.py +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/libs/helpers.js +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/module.py +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/modules/__init__.py +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/modules/crypto/__init__.py +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/modules/crypto/crypto.js +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion/modules/tls_unpinning/__init__.py +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion.egg-info/dependency_links.txt +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion.egg-info/entry_points.txt +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion.egg-info/requires.txt +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/frida_fusion.egg-info/top_level.txt +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/setup.cfg +0 -0
- {frida-fusion-0.1.5 → frida_fusion-0.1.7}/setup.py +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022, Helvio Junior
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: frida-fusion
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Hook your mobile tests with Frida
|
|
5
5
|
Author-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
|
|
6
6
|
Maintainer-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
|
|
@@ -12,7 +12,6 @@ Keywords: Frida Fusion,Frida,Frida Scripts,development,red team
|
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Intended Audience :: System Administrators
|
|
15
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
16
15
|
Classifier: Natural Language :: English
|
|
17
16
|
Classifier: Operating System :: OS Independent
|
|
18
17
|
Classifier: Programming Language :: Python
|
|
@@ -27,6 +26,12 @@ Classifier: Topic :: Security
|
|
|
27
26
|
Classifier: Topic :: Utilities
|
|
28
27
|
Requires-Python: <4,>=3.9
|
|
29
28
|
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: colorama
|
|
31
|
+
Requires-Dist: requests>=2.23.0
|
|
32
|
+
Requires-Dist: frida>=15.1.17
|
|
33
|
+
Requires-Dist: frida-tools>=10.8.0
|
|
34
|
+
Dynamic: license-file
|
|
30
35
|
|
|
31
36
|
# Frida Fusion
|
|
32
37
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
__version__ = '0.1.
|
|
1
|
+
__version__ = '0.1.7'
|
|
2
2
|
__title__ = "Frida Fusion"
|
|
3
3
|
__description__ = "📱 frida-fusion - runtime mobile exploration"
|
|
4
4
|
__url__ = "https://github.com/helviojunior/frida-fusion"
|
|
5
|
-
__build__ =
|
|
5
|
+
__build__ = 0x4760258
|
|
6
6
|
__author__ = "Helvio Junior (M4v3r1ck)"
|
|
7
7
|
__author_email__ = "helvio_junior@hotmail.com"
|
|
8
8
|
__license__ = "GPL-3.0"
|
|
@@ -168,13 +168,15 @@ class Configuration(object):
|
|
|
168
168
|
|
|
169
169
|
if args.show_time:
|
|
170
170
|
Configuration.print_timestamp = True
|
|
171
|
+
Logger.print_timestamp = Configuration.print_timestamp
|
|
171
172
|
|
|
172
|
-
if str(args.debug_level).upper() not in
|
|
173
|
+
if str(args.debug_level).upper() not in Logger.level_map.keys():
|
|
173
174
|
Color.pl(
|
|
174
175
|
'{!} {R}error: invalid debug level{R}{W}\r\n')
|
|
175
176
|
Configuration.mandatory()
|
|
176
177
|
|
|
177
|
-
Configuration.debug_level =
|
|
178
|
+
Configuration.debug_level = Logger.level_map.get(str(args.debug_level).upper(), 0)
|
|
179
|
+
Logger.debug_level = Configuration.debug_level
|
|
178
180
|
|
|
179
181
|
Logger.pl(' {C}min debug level:{O} %s{W}' % str(args.debug_level).upper())
|
|
180
182
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from .libs.color import Color
|
|
6
|
+
from .libs.scriptlocation import ScriptLocation
|
|
6
7
|
|
|
7
8
|
try:
|
|
8
9
|
from .config import Configuration
|
|
@@ -15,7 +16,6 @@ import frida
|
|
|
15
16
|
import json
|
|
16
17
|
import base64
|
|
17
18
|
import os
|
|
18
|
-
import threading
|
|
19
19
|
import signal
|
|
20
20
|
import sys
|
|
21
21
|
import re
|
|
@@ -29,43 +29,6 @@ from .libs.logger import Logger
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class Fusion(object):
|
|
32
|
-
class ScriptLocation(object):
|
|
33
|
-
def __init__(self, file_name: str = "<unknown>", function_name: str = "<unknown>", line: str = "<unknown>"):
|
|
34
|
-
self.file_name = file_name if file_name is not None else "<unknown>"
|
|
35
|
-
self.function_name = function_name if function_name is not None else "<unknown>"
|
|
36
|
-
self.line = str(line) if line is not None else "<unknown>"
|
|
37
|
-
|
|
38
|
-
def get_int_line(self):
|
|
39
|
-
try:
|
|
40
|
-
return int(self.line)
|
|
41
|
-
except:
|
|
42
|
-
return 0
|
|
43
|
-
|
|
44
|
-
def __str__(self):
|
|
45
|
-
return f"{self.file_name}:{self.line} ({self.function_name})"
|
|
46
|
-
|
|
47
|
-
def __repr__(self):
|
|
48
|
-
return str(self)
|
|
49
|
-
|
|
50
|
-
@staticmethod
|
|
51
|
-
def parse_from_dict(data: dict):
|
|
52
|
-
|
|
53
|
-
file_name = Path(data.get("file_name", "unknown")).name
|
|
54
|
-
function_name = data.get("function_name", "unknown")
|
|
55
|
-
line = str(data.get("line", -1))
|
|
56
|
-
|
|
57
|
-
if 'fileName' in data.keys():
|
|
58
|
-
file_name = Path(data.get("fileName", file_name)).name
|
|
59
|
-
|
|
60
|
-
if 'lineNumber' in data.keys():
|
|
61
|
-
line = str(data.get("lineNumber", line))
|
|
62
|
-
|
|
63
|
-
return Fusion.ScriptLocation(
|
|
64
|
-
file_name=file_name,
|
|
65
|
-
function_name=function_name,
|
|
66
|
-
line=line
|
|
67
|
-
)
|
|
68
|
-
|
|
69
32
|
running = True
|
|
70
33
|
debug = True
|
|
71
34
|
print_timestamp = False
|
|
@@ -87,7 +50,12 @@ class Fusion(object):
|
|
|
87
50
|
t.start()
|
|
88
51
|
|
|
89
52
|
def signal_handler(self, sig, frame):
|
|
53
|
+
if Logger.debug_level <= 2:
|
|
54
|
+
Logger.debug_level = 2
|
|
55
|
+
|
|
56
|
+
Fusion.running = False
|
|
90
57
|
Logger.pl('\n{!} {O}interrupted, shutting down...{W}\n')
|
|
58
|
+
time.sleep(0.5)
|
|
91
59
|
self.done.set()
|
|
92
60
|
|
|
93
61
|
def wait(self):
|
|
@@ -117,17 +85,17 @@ class Fusion(object):
|
|
|
117
85
|
|
|
118
86
|
return self.device
|
|
119
87
|
|
|
120
|
-
def translate_location(self, location: dict) ->
|
|
88
|
+
def translate_location(self, location: dict) -> ScriptLocation:
|
|
121
89
|
if location is None or not isinstance(location, dict):
|
|
122
|
-
return
|
|
90
|
+
return ScriptLocation()
|
|
123
91
|
|
|
124
|
-
loc =
|
|
92
|
+
loc = ScriptLocation.parse_from_dict(location)
|
|
125
93
|
|
|
126
94
|
if loc.file_name != "fusion_bundle.js":
|
|
127
95
|
return loc
|
|
128
96
|
|
|
129
97
|
return next(iter([
|
|
130
|
-
|
|
98
|
+
ScriptLocation(
|
|
131
99
|
file_name=k,
|
|
132
100
|
function_name=loc.function_name,
|
|
133
101
|
line=str(1 + loc.get_int_line() - v[0])
|
|
@@ -136,6 +104,25 @@ class Fusion(object):
|
|
|
136
104
|
if v[0] <= loc.get_int_line() <= v[1]
|
|
137
105
|
]), loc)
|
|
138
106
|
|
|
107
|
+
@classmethod
|
|
108
|
+
def print_message(cls, level: str = "*", message: str = "",
|
|
109
|
+
script_location: ScriptLocation = None):
|
|
110
|
+
|
|
111
|
+
if Fusion.running is False and Logger.debug_level >= 2:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
if script_location is None:
|
|
115
|
+
script_location = ScriptLocation(
|
|
116
|
+
file_name=Fusion._script_name
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
Logger.print_message(
|
|
120
|
+
level=level,
|
|
121
|
+
message=message,
|
|
122
|
+
script_location=script_location,
|
|
123
|
+
filename_col_len=Fusion.max_filename
|
|
124
|
+
)
|
|
125
|
+
|
|
139
126
|
def load_all_scripts(self):
|
|
140
127
|
self.script_trace = {}
|
|
141
128
|
offset = 1
|
|
@@ -200,6 +187,8 @@ class Fusion(object):
|
|
|
200
187
|
if len(file_name) > Fusion.max_filename:
|
|
201
188
|
Fusion.max_filename = len(file_name)
|
|
202
189
|
|
|
190
|
+
Logger.filename_col_len = Fusion.max_filename
|
|
191
|
+
|
|
203
192
|
try:
|
|
204
193
|
s = self.session.create_script(src, name="fusion_bundle")
|
|
205
194
|
s.on("message", self.make_handler("fusion_bundle.js")) # register the message handler
|
|
@@ -247,9 +236,13 @@ class Fusion(object):
|
|
|
247
236
|
|
|
248
237
|
def make_handler(self, script_name):
|
|
249
238
|
def handler(message, payload):
|
|
239
|
+
|
|
240
|
+
if not Fusion.running:
|
|
241
|
+
return
|
|
242
|
+
|
|
250
243
|
if message["type"] == "send":
|
|
251
244
|
try:
|
|
252
|
-
script_location =
|
|
245
|
+
script_location = ScriptLocation()
|
|
253
246
|
jData = message.get("payload", {})
|
|
254
247
|
if isinstance(jData, str):
|
|
255
248
|
jData = json.loads(message["payload"])
|
|
@@ -261,6 +254,19 @@ class Fusion(object):
|
|
|
261
254
|
jData = jData.get("payload", {})
|
|
262
255
|
script_location = self.translate_location(location)
|
|
263
256
|
|
|
257
|
+
if isinstance(jData, str):
|
|
258
|
+
msg = jData
|
|
259
|
+
try:
|
|
260
|
+
msg = base64.b64encode(jData.encode("UTF-8"))
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
jData = {
|
|
265
|
+
"type": "message",
|
|
266
|
+
"level": "I",
|
|
267
|
+
"message": msg
|
|
268
|
+
}
|
|
269
|
+
|
|
264
270
|
if script_location.file_name == "<unknown>":
|
|
265
271
|
script_location.file_name = script_name
|
|
266
272
|
|
|
@@ -272,10 +278,10 @@ class Fusion(object):
|
|
|
272
278
|
msg = base64.b64decode(msg).decode("UTF-8")
|
|
273
279
|
except:
|
|
274
280
|
pass
|
|
275
|
-
self.
|
|
281
|
+
self.print_message(mLevel, msg, script_location=script_location)
|
|
276
282
|
|
|
277
283
|
elif mType == "key_value_data":
|
|
278
|
-
self.
|
|
284
|
+
self.print_message("V", "RAW JSON:\n %s" % (
|
|
279
285
|
json.dumps(jData, indent=4).replace("\n", "\n ")
|
|
280
286
|
), script_location=script_location)
|
|
281
287
|
|
|
@@ -305,7 +311,7 @@ class Fusion(object):
|
|
|
305
311
|
|
|
306
312
|
# Legacy
|
|
307
313
|
elif mType == "data":
|
|
308
|
-
self.
|
|
314
|
+
self.print_message("V", "RAW JSON:\n %s" % (
|
|
309
315
|
json.dumps(jData, indent=4).replace("\n", "\n ")
|
|
310
316
|
), script_location=script_location)
|
|
311
317
|
|
|
@@ -329,19 +335,19 @@ class Fusion(object):
|
|
|
329
335
|
|
|
330
336
|
elif mType == "java-uncaught":
|
|
331
337
|
self.insert_history('frida', json.dumps(jData))
|
|
332
|
-
self.
|
|
338
|
+
self.print_message("E", jData.get('stack', ''), script_location=script_location)
|
|
333
339
|
|
|
334
340
|
else:
|
|
335
|
-
self.
|
|
341
|
+
self.print_message(mLevel, message, script_location=script_location)
|
|
336
342
|
|
|
337
343
|
except Exception as err:
|
|
338
|
-
script_location =
|
|
339
|
-
self.
|
|
340
|
-
self.
|
|
344
|
+
script_location = ScriptLocation(file_name=Fusion._script_name)
|
|
345
|
+
self.print_message("E", message, script_location=script_location)
|
|
346
|
+
self.print_message("E", payload, script_location=script_location)
|
|
341
347
|
self.print_exception(err)
|
|
342
348
|
|
|
343
349
|
else:
|
|
344
|
-
script_location =
|
|
350
|
+
script_location = ScriptLocation.parse_from_dict(message)
|
|
345
351
|
try:
|
|
346
352
|
if message["type"] == "error":
|
|
347
353
|
description = message.get('description', '')
|
|
@@ -375,18 +381,18 @@ class Fusion(object):
|
|
|
375
381
|
"stack": stack
|
|
376
382
|
}))
|
|
377
383
|
|
|
378
|
-
self.
|
|
379
|
-
|
|
384
|
+
self.print_message("F", description + stack,
|
|
385
|
+
script_location=script_location)
|
|
380
386
|
Fusion.running = False
|
|
381
387
|
time.sleep(0.2)
|
|
382
388
|
Logger.pl('\n{+} {O}Exiting...{O}{W}')
|
|
383
389
|
self.done.set()
|
|
384
390
|
else:
|
|
385
|
-
self.
|
|
386
|
-
self.
|
|
391
|
+
self.print_message("I", message, script_location=script_location)
|
|
392
|
+
self.print_message("I", payload, script_location=script_location)
|
|
387
393
|
except:
|
|
388
|
-
self.
|
|
389
|
-
self.
|
|
394
|
+
self.print_message("I", message, script_location=script_location)
|
|
395
|
+
self.print_message("I", payload, script_location=script_location)
|
|
390
396
|
|
|
391
397
|
return handler
|
|
392
398
|
|
|
@@ -404,7 +410,7 @@ class Fusion(object):
|
|
|
404
410
|
self.done.set()
|
|
405
411
|
|
|
406
412
|
def _raise_key_value_event(self,
|
|
407
|
-
script_location:
|
|
413
|
+
script_location: ScriptLocation = None,
|
|
408
414
|
stack_trace: str = None,
|
|
409
415
|
module: str = None,
|
|
410
416
|
received_data: dict = None):
|
|
@@ -417,10 +423,10 @@ class Fusion(object):
|
|
|
417
423
|
received_data=received_data
|
|
418
424
|
)
|
|
419
425
|
except Exception as e:
|
|
420
|
-
self.
|
|
426
|
+
self.print_message("E", f"Error resizing event to module {m.name}: {str(e)}")
|
|
421
427
|
|
|
422
428
|
def _raise_data_event(self,
|
|
423
|
-
script_location:
|
|
429
|
+
script_location: ScriptLocation = None,
|
|
424
430
|
stack_trace: str = None,
|
|
425
431
|
received_data: str = None):
|
|
426
432
|
for m in self._modules:
|
|
@@ -431,7 +437,7 @@ class Fusion(object):
|
|
|
431
437
|
received_data=received_data
|
|
432
438
|
)
|
|
433
439
|
except Exception as e:
|
|
434
|
-
self.
|
|
440
|
+
self.print_message("E", f"Error resizing event to module {m.name}: {str(e)}")
|
|
435
441
|
|
|
436
442
|
@classmethod
|
|
437
443
|
def insert_history(cls, source: str, data: str, stack_trace: str = ''):
|
|
@@ -449,81 +455,20 @@ class Fusion(object):
|
|
|
449
455
|
kwargs = Fusion._db_jobs.get()
|
|
450
456
|
db.insert_history(**kwargs)
|
|
451
457
|
|
|
452
|
-
@classmethod
|
|
453
|
-
def _print_message(cls, level: str = "*", message: str = "",
|
|
454
|
-
script_location: Fusion.ScriptLocation = None):
|
|
455
|
-
|
|
456
|
-
if not Fusion.running and not Configuration.debug_level >= 2:
|
|
457
|
-
return
|
|
458
|
-
|
|
459
|
-
if level is None:
|
|
460
|
-
level = "*"
|
|
461
|
-
|
|
462
|
-
dbg_tag = next(iter([
|
|
463
|
-
k
|
|
464
|
-
for k in Color.level_map.keys()
|
|
465
|
-
if level.upper() == k
|
|
466
|
-
]), Color.level_tag.get(level, "I"))
|
|
467
|
-
|
|
468
|
-
dbg_idx = Color.level_map.get(dbg_tag, 0)
|
|
469
|
-
|
|
470
|
-
if Configuration.debug_level > dbg_idx:
|
|
471
|
-
return
|
|
472
|
-
|
|
473
|
-
prefix = ""
|
|
474
|
-
if Configuration.print_timestamp:
|
|
475
|
-
ts = datetime.now()
|
|
476
|
-
stamp = f"{ts:%H:%M:%S}.{int(ts.microsecond / 1000):03d}"
|
|
477
|
-
prefix += f"\033[2m{stamp.ljust(13)}{Color.color_reset}"
|
|
478
|
-
|
|
479
|
-
fg_color = Color.color_level[dbg_idx]
|
|
480
|
-
tag_color = Color.color_tags[dbg_idx]
|
|
481
|
-
|
|
482
|
-
if script_location is None:
|
|
483
|
-
script_location = Fusion.ScriptLocation(
|
|
484
|
-
file_name=Fusion._script_name
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
if script_location.file_name == "frida/node_modules/frida-java-bridge/lib/class-factory.js":
|
|
488
|
-
file_name = "frida/.../class-factory.js"
|
|
489
|
-
else:
|
|
490
|
-
file_name = str(Path(script_location.file_name).name)
|
|
491
|
-
|
|
492
|
-
prefix += (f"{fg_color}{file_name.rjust(Fusion.max_filename)}"
|
|
493
|
-
f"{Color.color_reset}\033[2m:{str(script_location.line).ljust(10)}"
|
|
494
|
-
f"{Color.color_reset} ")
|
|
495
|
-
prefix_len = len(Color.escape_ansi(prefix))
|
|
496
|
-
|
|
497
|
-
f_message = ""
|
|
498
|
-
if message is None:
|
|
499
|
-
message = ""
|
|
500
|
-
|
|
501
|
-
if isinstance(message, dict):
|
|
502
|
-
try:
|
|
503
|
-
message = json.dumps(message, ident=4)
|
|
504
|
-
except:
|
|
505
|
-
message = str(message)
|
|
506
|
-
|
|
507
|
-
for line in message.split("\n"):
|
|
508
|
-
if f_message == "":
|
|
509
|
-
f_message += (f"{prefix}{tag_color} {dbg_tag} {Color.color_reset} "
|
|
510
|
-
f"{fg_color}{line}{Color.color_reset}")
|
|
511
|
-
else:
|
|
512
|
-
f_message += (f"\n{''.rjust(prefix_len)}{tag_color} {dbg_tag} "
|
|
513
|
-
f"{Color.color_reset} {fg_color}{line}{Color.color_reset}")
|
|
514
|
-
|
|
515
|
-
Logger.pl(f_message)
|
|
516
|
-
|
|
517
458
|
@classmethod
|
|
518
459
|
def print_exception(cls, err):
|
|
519
|
-
Logger.pl('\n{!} {R}Error:{O} %s{W}' % str(err))
|
|
520
|
-
Logger.pl('\n{!} {O}Full stack trace below')
|
|
521
460
|
from traceback import format_exc
|
|
522
|
-
err_txt =
|
|
461
|
+
err_txt = '{R}Error:{O} %s{W}' % str(err)
|
|
462
|
+
err_txt += '{O}Full stack trace below'
|
|
463
|
+
err_txt += format_exc().strip()
|
|
464
|
+
|
|
523
465
|
err_txt = err_txt.replace('\n', '\n{W}{!} {W} ')
|
|
524
466
|
err_txt = err_txt.replace(' File', '{W}{D}File')
|
|
525
467
|
err_txt = err_txt.replace(' Exception: ', '{R}Exception: {O}')
|
|
526
|
-
|
|
468
|
+
cls.print_message(
|
|
469
|
+
level="E",
|
|
470
|
+
message=Color.s('{!} ' + err_txt)
|
|
471
|
+
)
|
|
527
472
|
|
|
528
473
|
@classmethod
|
|
529
474
|
def check_frida_native_exception(cls, evt: dict) -> bool:
|
|
@@ -52,21 +52,6 @@ class Color(object):
|
|
|
52
52
|
"\033[31m\033[48;2;60;60;60m", # Fatal
|
|
53
53
|
]
|
|
54
54
|
|
|
55
|
-
level_map = {
|
|
56
|
-
"V": 0,
|
|
57
|
-
"D": 1,
|
|
58
|
-
"I": 2,
|
|
59
|
-
"W": 3,
|
|
60
|
-
"E": 4,
|
|
61
|
-
"F": 5,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
level_tag = {
|
|
65
|
-
"*": "D",
|
|
66
|
-
"+": "I",
|
|
67
|
-
"-": "W",
|
|
68
|
-
}
|
|
69
|
-
|
|
70
55
|
last_sameline_length = 0
|
|
71
56
|
|
|
72
57
|
@staticmethod
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# -*- coding: UTF-8 -*-
|
|
3
|
+
import base64
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .scriptlocation import ScriptLocation
|
|
10
|
+
from ..libs.color import Color
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Logger(object):
|
|
14
|
+
''' Helper object for easily printing colored text to the terminal. '''
|
|
15
|
+
|
|
16
|
+
out_file = ''
|
|
17
|
+
debug_level = 0
|
|
18
|
+
print_timestamp = True
|
|
19
|
+
filename_col_len = 0
|
|
20
|
+
|
|
21
|
+
level_map = {
|
|
22
|
+
"V": 0,
|
|
23
|
+
"D": 1,
|
|
24
|
+
"I": 2,
|
|
25
|
+
"W": 3,
|
|
26
|
+
"E": 4,
|
|
27
|
+
"F": 5,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
level_tag = {
|
|
31
|
+
"*": "D",
|
|
32
|
+
"+": "I",
|
|
33
|
+
"-": "W",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def pl(text):
|
|
38
|
+
'''Prints text using colored format with trailing new line.'''
|
|
39
|
+
Color.pl(text)
|
|
40
|
+
|
|
41
|
+
if Logger.out_file != '':
|
|
42
|
+
try:
|
|
43
|
+
with open(Logger.out_file, "a") as text_file:
|
|
44
|
+
text_file.write(Color.sc(text) + '\n')
|
|
45
|
+
except:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def pl_file(text):
|
|
50
|
+
'''Prints text using colored format with trailing new line.'''
|
|
51
|
+
|
|
52
|
+
if Logger.out_file != '':
|
|
53
|
+
try:
|
|
54
|
+
with open(Logger.out_file, "a") as text_file:
|
|
55
|
+
text_file.write(Color.escape_ansi(Color.sc(text)) + '\n')
|
|
56
|
+
except:
|
|
57
|
+
Color.pl(text)
|
|
58
|
+
else:
|
|
59
|
+
Color.pl(text)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def _get_caller_info(cls, stack_index: int = 1) -> ScriptLocation:
|
|
63
|
+
"""Retrieves information about the calling script, function, and line number."""
|
|
64
|
+
# inspect.stack() returns a list of frame records.
|
|
65
|
+
# Each frame record is a tuple containing:
|
|
66
|
+
# (frame object, filename, line number, function name, list of lines of context, index of current line in context)
|
|
67
|
+
|
|
68
|
+
# The first element (index 0) is the current function (get_caller_info).
|
|
69
|
+
# The second element (index 1) is the caller of get_caller_info.
|
|
70
|
+
caller_frame = inspect.stack()[stack_index]
|
|
71
|
+
|
|
72
|
+
# Extract information from the caller's frame
|
|
73
|
+
filename = caller_frame.filename
|
|
74
|
+
line_number = caller_frame.lineno
|
|
75
|
+
function_name = caller_frame.function
|
|
76
|
+
|
|
77
|
+
# Optionally, get the base name of the script for cleaner output
|
|
78
|
+
script_name = Path(filename).name
|
|
79
|
+
|
|
80
|
+
return ScriptLocation(
|
|
81
|
+
file_name=script_name,
|
|
82
|
+
function_name=function_name,
|
|
83
|
+
line=str(line_number)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def json_serial(obj):
|
|
88
|
+
"""JSON serializer for objects not serializable by default json code"""
|
|
89
|
+
|
|
90
|
+
if isinstance(obj, (datetime.datetime, datetime.date)):
|
|
91
|
+
# obj = obj.astimezone(datetime.timezone(datetime.timedelta(hours=0), 'Z'))
|
|
92
|
+
return obj.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
93
|
+
|
|
94
|
+
if isinstance(obj, bytes):
|
|
95
|
+
return base64.b64encode(obj).decode("UTF-8")
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
return str(obj)
|
|
99
|
+
except:
|
|
100
|
+
return f"[ERROR] Type %s not serializable{type(obj)}"
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def print_message(cls, level: str = "*", message: str = "",
|
|
104
|
+
script_location: ScriptLocation = None, filename_col_len: int = 26):
|
|
105
|
+
|
|
106
|
+
if level is None:
|
|
107
|
+
level = "*"
|
|
108
|
+
|
|
109
|
+
dbg_tag = next(iter([
|
|
110
|
+
k
|
|
111
|
+
for k in Logger.level_map.keys()
|
|
112
|
+
if level.upper() == k
|
|
113
|
+
]), Logger.level_tag.get(level, "I"))
|
|
114
|
+
|
|
115
|
+
dbg_idx = Logger.level_map.get(dbg_tag, 0)
|
|
116
|
+
|
|
117
|
+
if Logger.debug_level > dbg_idx:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if filename_col_len < 26:
|
|
121
|
+
filename_col_len = 26
|
|
122
|
+
|
|
123
|
+
if Logger.filename_col_len > filename_col_len:
|
|
124
|
+
filename_col_len = Logger.filename_col_len
|
|
125
|
+
|
|
126
|
+
prefix = ""
|
|
127
|
+
if Logger.print_timestamp:
|
|
128
|
+
ts = datetime.datetime.now()
|
|
129
|
+
stamp = f"{ts:%H:%M:%S}.{int(ts.microsecond / 1000):03d}"
|
|
130
|
+
prefix += f"\033[2m{stamp.ljust(13)}{Color.color_reset}"
|
|
131
|
+
|
|
132
|
+
fg_color = Color.color_level[dbg_idx]
|
|
133
|
+
tag_color = Color.color_tags[dbg_idx]
|
|
134
|
+
|
|
135
|
+
if script_location is None:
|
|
136
|
+
script_location = cls._get_caller_info(stack_index=2)
|
|
137
|
+
|
|
138
|
+
if script_location.file_name == "frida/node_modules/frida-java-bridge/lib/class-factory.js":
|
|
139
|
+
file_name = "frida/.../class-factory.js"
|
|
140
|
+
else:
|
|
141
|
+
file_name = str(Path(script_location.file_name).name)
|
|
142
|
+
|
|
143
|
+
if len(file_name) > filename_col_len:
|
|
144
|
+
file_name = f"{file_name[0:filename_col_len-3]}..."
|
|
145
|
+
|
|
146
|
+
prefix += (f"{fg_color}{file_name.rjust(filename_col_len)}"
|
|
147
|
+
f"{Color.color_reset}\033[2m:{str(script_location.line).ljust(10)}"
|
|
148
|
+
f"{Color.color_reset} ")
|
|
149
|
+
prefix_len = len(Color.escape_ansi(prefix))
|
|
150
|
+
|
|
151
|
+
f_message = ""
|
|
152
|
+
if message is None:
|
|
153
|
+
message = ""
|
|
154
|
+
|
|
155
|
+
if isinstance(message, dict):
|
|
156
|
+
try:
|
|
157
|
+
message = json.dumps(message, ident=4, default=Logger.json_serial)
|
|
158
|
+
except:
|
|
159
|
+
message = str(message)
|
|
160
|
+
|
|
161
|
+
for line in message.split("\n"):
|
|
162
|
+
if f_message == "":
|
|
163
|
+
f_message += (f"{prefix}{tag_color} {dbg_tag} {Color.color_reset} "
|
|
164
|
+
f"{fg_color}{line}{Color.color_reset}")
|
|
165
|
+
else:
|
|
166
|
+
f_message += (f"\n{''.rjust(prefix_len)}{tag_color} {dbg_tag} "
|
|
167
|
+
f"{Color.color_reset} {fg_color}{line}{Color.color_reset}")
|
|
168
|
+
|
|
169
|
+
Logger.pl(f_message)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ScriptLocation(object):
|
|
5
|
+
def __init__(self, file_name: str = "<unknown>", function_name: str = "<unknown>", line: str = "<unknown>"):
|
|
6
|
+
self.file_name = file_name if file_name is not None else "<unknown>"
|
|
7
|
+
self.function_name = function_name if function_name is not None else "<unknown>"
|
|
8
|
+
self.line = str(line) if line is not None else "<unknown>"
|
|
9
|
+
|
|
10
|
+
def get_int_line(self):
|
|
11
|
+
try:
|
|
12
|
+
return int(self.line)
|
|
13
|
+
except Exception:
|
|
14
|
+
return 0
|
|
15
|
+
|
|
16
|
+
def __str__(self):
|
|
17
|
+
return f"{self.file_name}:{self.line} ({self.function_name})"
|
|
18
|
+
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
return str(self)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def parse_from_dict(data: dict):
|
|
24
|
+
|
|
25
|
+
file_name = Path(data.get("file_name", "unknown")).name
|
|
26
|
+
function_name = data.get("function_name", "unknown")
|
|
27
|
+
line = str(data.get("line", -1))
|
|
28
|
+
|
|
29
|
+
if 'fileName' in data.keys():
|
|
30
|
+
file_name = Path(data.get("fileName", file_name)).name
|
|
31
|
+
|
|
32
|
+
if 'lineNumber' in data.keys():
|
|
33
|
+
line = str(data.get("lineNumber", line))
|
|
34
|
+
|
|
35
|
+
return ScriptLocation(
|
|
36
|
+
file_name=file_name,
|
|
37
|
+
function_name=function_name,
|
|
38
|
+
line=line
|
|
39
|
+
)
|
|
@@ -5,12 +5,9 @@ import string
|
|
|
5
5
|
|
|
6
6
|
from frida_fusion.libs.logger import Logger
|
|
7
7
|
from frida_fusion.libs.database import Database
|
|
8
|
+
from frida_fusion.libs.scriptlocation import ScriptLocation
|
|
8
9
|
from frida_fusion.module import ModuleBase
|
|
9
10
|
|
|
10
|
-
from typing import TYPE_CHECKING
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
from frida_fusion.fusion import Fusion # só no type checker
|
|
13
|
-
|
|
14
11
|
|
|
15
12
|
class Crypto(ModuleBase):
|
|
16
13
|
class CryptoDB(Database):
|
|
@@ -287,7 +284,7 @@ class Crypto(ModuleBase):
|
|
|
287
284
|
]
|
|
288
285
|
|
|
289
286
|
def key_value_event(self,
|
|
290
|
-
script_location:
|
|
287
|
+
script_location: ScriptLocation = None,
|
|
291
288
|
stack_trace: str = None,
|
|
292
289
|
module: str = None,
|
|
293
290
|
received_data: dict = None
|
|
@@ -319,6 +316,12 @@ class Crypto(ModuleBase):
|
|
|
319
316
|
received_data.get('output', ''),
|
|
320
317
|
stack_trace=stack_trace)
|
|
321
318
|
|
|
319
|
+
Logger.print_message(
|
|
320
|
+
level="D",
|
|
321
|
+
message=f"Cipher doFinal received\n{stack_trace}",
|
|
322
|
+
script_location=script_location
|
|
323
|
+
)
|
|
324
|
+
|
|
322
325
|
elif module == "messageDigest.update":
|
|
323
326
|
hashcode = received_data.get('hashcode', None)
|
|
324
327
|
algorithm = received_data.get('algorithm', None)
|
|
@@ -332,10 +335,28 @@ class Crypto(ModuleBase):
|
|
|
332
335
|
bOutput = received_data.get('output', None)
|
|
333
336
|
self._crypto_db.insert_digest(hashcode, algorithm, bInput, bOutput, stack_trace=stack_trace)
|
|
334
337
|
|
|
338
|
+
hash_hex = ""
|
|
339
|
+
if bOutput is not None:
|
|
340
|
+
try:
|
|
341
|
+
if isinstance(bOutput, bytes):
|
|
342
|
+
hash_hex = ''.join('{:02x}'.format(b) for b in bOutput)
|
|
343
|
+
else:
|
|
344
|
+
hash_hex = ''.join('{:02x}'.format(b) for b in base64.b64decode(bOutput))
|
|
345
|
+
except:
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
# Do not print TLS certificate verification hash
|
|
349
|
+
if 'com.android.org.conscrypt.ConscryptEngine.verifyCertificateChain' not in stack_trace:
|
|
350
|
+
Logger.print_message(
|
|
351
|
+
level="D",
|
|
352
|
+
message=f"Message digest\nAlgorithm: {algorithm}\nHash: {hash_hex}\n{stack_trace}",
|
|
353
|
+
script_location=script_location
|
|
354
|
+
)
|
|
355
|
+
|
|
335
356
|
return True
|
|
336
357
|
|
|
337
358
|
def data_event(self,
|
|
338
|
-
script_location:
|
|
359
|
+
script_location: ScriptLocation = None,
|
|
339
360
|
stack_trace: str = None,
|
|
340
361
|
received_data: str = None
|
|
341
362
|
) -> bool:
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import errno
|
|
2
2
|
import os.path
|
|
3
|
-
import tempfile
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from frida_fusion.libs.logger import Logger
|
|
5
|
+
from frida_fusion.libs.scriptlocation import ScriptLocation
|
|
6
6
|
from frida_fusion.module import ModuleBase
|
|
7
7
|
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from frida_fusion.fusion import Fusion # só no type checker
|
|
12
|
-
|
|
13
8
|
|
|
14
9
|
class TlsUnpinning(ModuleBase):
|
|
15
10
|
|
|
@@ -49,7 +44,7 @@ class TlsUnpinning(ModuleBase):
|
|
|
49
44
|
]
|
|
50
45
|
|
|
51
46
|
def key_value_event(self,
|
|
52
|
-
script_location:
|
|
47
|
+
script_location: ScriptLocation = None,
|
|
53
48
|
stack_trace: str = None,
|
|
54
49
|
module: str = None,
|
|
55
50
|
received_data: dict = None
|
|
@@ -57,7 +52,7 @@ class TlsUnpinning(ModuleBase):
|
|
|
57
52
|
return True
|
|
58
53
|
|
|
59
54
|
def data_event(self,
|
|
60
|
-
script_location:
|
|
55
|
+
script_location: ScriptLocation = None,
|
|
61
56
|
stack_trace: str = None,
|
|
62
57
|
received_data: str = None
|
|
63
58
|
) -> bool:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: frida-fusion
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Hook your mobile tests with Frida
|
|
5
5
|
Author-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
|
|
6
6
|
Maintainer-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
|
|
@@ -12,7 +12,6 @@ Keywords: Frida Fusion,Frida,Frida Scripts,development,red team
|
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Intended Audience :: System Administrators
|
|
15
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
16
15
|
Classifier: Natural Language :: English
|
|
17
16
|
Classifier: Operating System :: OS Independent
|
|
18
17
|
Classifier: Programming Language :: Python
|
|
@@ -27,6 +26,12 @@ Classifier: Topic :: Security
|
|
|
27
26
|
Classifier: Topic :: Utilities
|
|
28
27
|
Requires-Python: <4,>=3.9
|
|
29
28
|
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: colorama
|
|
31
|
+
Requires-Dist: requests>=2.23.0
|
|
32
|
+
Requires-Dist: frida>=15.1.17
|
|
33
|
+
Requires-Dist: frida-tools>=10.8.0
|
|
34
|
+
Dynamic: license-file
|
|
30
35
|
|
|
31
36
|
# Frida Fusion
|
|
32
37
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
LICENSE
|
|
1
2
|
README.md
|
|
2
3
|
pyproject.toml
|
|
3
4
|
setup.py
|
|
@@ -19,6 +20,7 @@ frida_fusion/libs/color.py
|
|
|
19
20
|
frida_fusion/libs/database.py
|
|
20
21
|
frida_fusion/libs/helpers.js
|
|
21
22
|
frida_fusion/libs/logger.py
|
|
23
|
+
frida_fusion/libs/scriptlocation.py
|
|
22
24
|
frida_fusion/modules/__init__.py
|
|
23
25
|
frida_fusion/modules/crypto/__init__.py
|
|
24
26
|
frida_fusion/modules/crypto/crypto.js
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools"
|
|
2
|
+
requires = ["setuptools>=80.0.0"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
@@ -8,13 +8,12 @@ name = "frida-fusion"
|
|
|
8
8
|
description = "Hook your mobile tests with Frida"
|
|
9
9
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
10
|
requires-python = ">=3.9,<4"
|
|
11
|
-
license =
|
|
11
|
+
license-files = ["LICEN[CS]E*"]
|
|
12
12
|
keywords = ["Frida Fusion", "Frida", "Frida Scripts", "development", "red team"]
|
|
13
13
|
classifiers = [
|
|
14
14
|
"Development Status :: 4 - Beta",
|
|
15
15
|
"Environment :: Console",
|
|
16
16
|
"Intended Audience :: System Administrators",
|
|
17
|
-
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
18
17
|
"Natural Language :: English",
|
|
19
18
|
"Operating System :: OS Independent",
|
|
20
19
|
"Programming Language :: Python",
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/python3
|
|
2
|
-
# -*- coding: UTF-8 -*-
|
|
3
|
-
|
|
4
|
-
import sys
|
|
5
|
-
from ..libs.color import Color
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Logger(object):
|
|
9
|
-
''' Helper object for easily printing colored text to the terminal. '''
|
|
10
|
-
|
|
11
|
-
out_file = ''
|
|
12
|
-
|
|
13
|
-
@staticmethod
|
|
14
|
-
def pl(text):
|
|
15
|
-
'''Prints text using colored format with trailing new line.'''
|
|
16
|
-
Color.pl(text)
|
|
17
|
-
|
|
18
|
-
if Logger.out_file != '':
|
|
19
|
-
try:
|
|
20
|
-
with open(Logger.out_file, "a") as text_file:
|
|
21
|
-
text_file.write(Color.sc(text) + '\n')
|
|
22
|
-
except:
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
@staticmethod
|
|
26
|
-
def pl_file(text):
|
|
27
|
-
'''Prints text using colored format with trailing new line.'''
|
|
28
|
-
|
|
29
|
-
if Logger.out_file != '':
|
|
30
|
-
try:
|
|
31
|
-
with open(Logger.out_file, "a") as text_file:
|
|
32
|
-
text_file.write(Color.escape_ansi(Color.sc(text)) + '\n')
|
|
33
|
-
except:
|
|
34
|
-
Color.pl(text)
|
|
35
|
-
else:
|
|
36
|
-
Color.pl(text)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|