maya-umbrella 0.5.0__py2.py3-none-any.whl → 0.6.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.
- maya_umbrella/__init__.py +6 -2
- maya_umbrella/__version__.py +1 -1
- maya_umbrella/cleaner.py +111 -0
- maya_umbrella/collector.py +411 -0
- maya_umbrella/constants.py +4 -0
- maya_umbrella/defender.py +161 -0
- maya_umbrella/filesystem.py +90 -3
- maya_umbrella/i18n.py +76 -0
- maya_umbrella/locales/en_US.json +19 -0
- maya_umbrella/locales/zh_CN.json +19 -0
- maya_umbrella/maya_funs.py +102 -2
- maya_umbrella/scanner.py +106 -0
- maya_umbrella/signatures.py +10 -0
- maya_umbrella/vaccine.py +18 -234
- maya_umbrella/vaccines/vaccine1.py +2 -1
- maya_umbrella/vaccines/vaccine2.py +14 -14
- maya_umbrella/vaccines/vaccine3.py +24 -16
- {maya_umbrella-0.5.0.dist-info → maya_umbrella-0.6.1.dist-info}/METADATA +66 -4
- maya_umbrella-0.6.1.dist-info/RECORD +28 -0
- maya_umbrella/core.py +0 -120
- maya_umbrella-0.5.0.dist-info/RECORD +0 -21
- {maya_umbrella-0.5.0.dist-info → maya_umbrella-0.6.1.dist-info}/LICENSE +0 -0
- {maya_umbrella-0.5.0.dist-info → maya_umbrella-0.6.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Import built-in modules
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
# Import local modules
|
|
6
|
+
from maya_umbrella.cleaner import MayaVirusCleaner
|
|
7
|
+
from maya_umbrella.collector import MayaVirusCollector
|
|
8
|
+
from maya_umbrella.filesystem import get_hooks
|
|
9
|
+
from maya_umbrella.filesystem import load_hook
|
|
10
|
+
from maya_umbrella.i18n import Translator
|
|
11
|
+
from maya_umbrella.log import setup_logger
|
|
12
|
+
from maya_umbrella.maya_funs import is_maya_standalone
|
|
13
|
+
from maya_umbrella.maya_funs import om
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Global list to store IDs of Maya callbacks
|
|
17
|
+
MAYA_UMBRELLA_CALLBACK_IDS = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _add_callbacks_id(id_):
|
|
21
|
+
"""Add a callback ID to the global list if it's not already present.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
id_ (int): ID of the callback to be added.
|
|
25
|
+
"""
|
|
26
|
+
global MAYA_UMBRELLA_CALLBACK_IDS
|
|
27
|
+
if id_ not in MAYA_UMBRELLA_CALLBACK_IDS:
|
|
28
|
+
MAYA_UMBRELLA_CALLBACK_IDS.append(id_)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MayaVirusDefender(object):
|
|
32
|
+
"""A class to defend against Maya viruses.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
_vaccines (list): List to store vaccines.
|
|
36
|
+
callback_maps (dict): Dictionary to map callback names to MSceneMessage constants.
|
|
37
|
+
auto_fix (bool): Whether to automatically fix issues.
|
|
38
|
+
logger (Logger): Logger object for logging purposes.
|
|
39
|
+
translator (Translator): Translator object for translation purposes.
|
|
40
|
+
collector (MayaVirusCollector): MayaVirusCollector object for collecting issues.
|
|
41
|
+
virus_cleaner (MayaVirusCleaner): MayaVirusCleaner object for fixing issues.
|
|
42
|
+
hooks (list): List of hooks to run.
|
|
43
|
+
"""
|
|
44
|
+
_vaccines = []
|
|
45
|
+
callback_maps = {
|
|
46
|
+
"after_open": om.MSceneMessage.kAfterOpen,
|
|
47
|
+
"maya_initialized": om.MSceneMessage.kMayaInitialized,
|
|
48
|
+
"after_import": om.MSceneMessage.kAfterImport,
|
|
49
|
+
"after_import_reference": om.MSceneMessage.kAfterImportReference,
|
|
50
|
+
"after_load_reference": om.MSceneMessage.kAfterLoadReference,
|
|
51
|
+
"before_save": om.MSceneMessage.kBeforeSave,
|
|
52
|
+
"before_import": om.MSceneMessage.kBeforeImport,
|
|
53
|
+
"before_load_reference": om.MSceneMessage.kBeforeLoadReference,
|
|
54
|
+
"before_import_reference": om.MSceneMessage.kBeforeImportReference,
|
|
55
|
+
"maya_exiting": om.MSceneMessage.kMayaExiting,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def __init__(self, auto_fix=True):
|
|
59
|
+
"""Initialize the MayaVirusDefender.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
auto_fix (bool): Whether to automatically fix issues.
|
|
63
|
+
"""
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
self.auto_fix = auto_fix
|
|
66
|
+
self.logger = setup_logger(logger)
|
|
67
|
+
self.translator = Translator()
|
|
68
|
+
self.collector = MayaVirusCollector(self.logger, self.translator)
|
|
69
|
+
self.virus_cleaner = MayaVirusCleaner(self.collector, self.logger)
|
|
70
|
+
self.hooks = get_hooks()
|
|
71
|
+
|
|
72
|
+
def run_hooks(self):
|
|
73
|
+
"""Run all hooks, only works in non-batch mode."""
|
|
74
|
+
if not is_maya_standalone():
|
|
75
|
+
for hook_file in self.hooks:
|
|
76
|
+
self.logger.debug("run_hook: %s", hook_file)
|
|
77
|
+
try:
|
|
78
|
+
load_hook(hook_file).hook(virus_cleaner=self.virus_cleaner)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.logger.debug("Error running hook: %s", e)
|
|
81
|
+
|
|
82
|
+
def collect(self):
|
|
83
|
+
"""Collect all issues related to the Maya virus."""
|
|
84
|
+
self.collector.collect()
|
|
85
|
+
|
|
86
|
+
def fix(self):
|
|
87
|
+
"""Fix all issues related to the Maya virus."""
|
|
88
|
+
self.virus_cleaner.fix()
|
|
89
|
+
|
|
90
|
+
def report(self):
|
|
91
|
+
"""Report all issues related to the Maya virus."""
|
|
92
|
+
self.collect()
|
|
93
|
+
self.collector.report()
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def have_issues(self):
|
|
97
|
+
"""Check if any issues are found.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
bool: True if any issues are found, False otherwise.
|
|
101
|
+
"""
|
|
102
|
+
return self.collector.have_issues
|
|
103
|
+
|
|
104
|
+
def setup(self):
|
|
105
|
+
"""Set up the MayaVirusDefender."""
|
|
106
|
+
self.virus_cleaner.setup_default_callbacks()
|
|
107
|
+
for name, callbacks in self.collector.registered_callbacks.items():
|
|
108
|
+
maya_callback = self.callback_maps[name]
|
|
109
|
+
self.logger.debug("%s setup.", name)
|
|
110
|
+
for func in callbacks:
|
|
111
|
+
_add_callbacks_id(om.MSceneMessage.addCallback(maya_callback, func))
|
|
112
|
+
for name, callbacks in self.callback_maps.items():
|
|
113
|
+
self.logger.debug("setup callback %s.", name)
|
|
114
|
+
_add_callbacks_id(om.MSceneMessage.addCallback(callbacks, self._callback))
|
|
115
|
+
|
|
116
|
+
def stop(self):
|
|
117
|
+
"""Stop the MayaVirusDefender."""
|
|
118
|
+
for ids in MAYA_UMBRELLA_CALLBACK_IDS:
|
|
119
|
+
self.logger.debug("remove callback. %s", ids)
|
|
120
|
+
om.MSceneMessage.removeCallback(ids)
|
|
121
|
+
MAYA_UMBRELLA_CALLBACK_IDS.remove(ids)
|
|
122
|
+
|
|
123
|
+
def get_unfixed_references(self):
|
|
124
|
+
"""Get the list of unfixed reference files.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
list: List of unfixed reference files.
|
|
128
|
+
"""
|
|
129
|
+
self.collect()
|
|
130
|
+
return self.collector.infected_reference_files
|
|
131
|
+
|
|
132
|
+
def _callback(self, *args, **kwargs):
|
|
133
|
+
"""Callback function for MayaVirusDefender.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
*args: Variable length argument list.
|
|
137
|
+
**kwargs: Arbitrary keyword arguments.
|
|
138
|
+
"""
|
|
139
|
+
if self.auto_fix:
|
|
140
|
+
self.collect()
|
|
141
|
+
self.fix()
|
|
142
|
+
self.run_hooks()
|
|
143
|
+
else:
|
|
144
|
+
self.report()
|
|
145
|
+
|
|
146
|
+
def start(self):
|
|
147
|
+
"""Start the MayaVirusDefender."""
|
|
148
|
+
self._callback()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@contextmanager
|
|
152
|
+
def context_defender():
|
|
153
|
+
"""Context manager for MayaVirusDefender.
|
|
154
|
+
|
|
155
|
+
Yields:
|
|
156
|
+
MayaVirusDefender: An instance of MayaVirusDefender.
|
|
157
|
+
"""
|
|
158
|
+
defender = MayaVirusDefender()
|
|
159
|
+
defender.stop()
|
|
160
|
+
yield defender
|
|
161
|
+
defender.setup()
|
maya_umbrella/filesystem.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
from contextlib import contextmanager
|
|
3
3
|
import glob
|
|
4
4
|
import importlib
|
|
5
|
-
import
|
|
5
|
+
import json
|
|
6
6
|
import os
|
|
7
7
|
import random
|
|
8
8
|
import re
|
|
@@ -53,6 +53,17 @@ def read_file(path):
|
|
|
53
53
|
return content
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def read_json(path):
|
|
57
|
+
"""Read the content of the file at the given path."""
|
|
58
|
+
options = {"encoding": "utf-8"} if PY3 else {}
|
|
59
|
+
with open(path, **options) as file_:
|
|
60
|
+
try:
|
|
61
|
+
content = json.load(file_)
|
|
62
|
+
except UnicodeDecodeError:
|
|
63
|
+
return {}
|
|
64
|
+
return content
|
|
65
|
+
|
|
66
|
+
|
|
56
67
|
def write_file(path, content):
|
|
57
68
|
"""Write the given content to the file at the given path."""
|
|
58
69
|
options = {"encoding": "utf-8"} if PY3 else {}
|
|
@@ -62,6 +73,21 @@ def write_file(path, content):
|
|
|
62
73
|
|
|
63
74
|
@contextmanager
|
|
64
75
|
def atomic_writes(src, mode, **options):
|
|
76
|
+
"""Context manager for atomic writes to a file.
|
|
77
|
+
|
|
78
|
+
This context manager ensures that the file is only written to disk if the write operation completes without errors.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
src (str): Path to the file to be written.
|
|
82
|
+
mode (str): Mode in which the file is opened, like 'r', 'w', 'a', etc.
|
|
83
|
+
**options: Arbitrary keyword arguments that are passed to the built-in open() function.
|
|
84
|
+
|
|
85
|
+
Yields:
|
|
86
|
+
file object: The opened file object.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
AttributeError: If the os module does not have the 'replace' function (Python 2 compatibility).
|
|
90
|
+
"""
|
|
65
91
|
temp_path = os.path.join(os.path.dirname(src), "._{}".format(id_generator()))
|
|
66
92
|
with open(temp_path, mode, **options) as f:
|
|
67
93
|
yield f
|
|
@@ -71,6 +97,7 @@ def atomic_writes(src, mode, **options):
|
|
|
71
97
|
shutil.move(temp_path, src)
|
|
72
98
|
|
|
73
99
|
|
|
100
|
+
|
|
74
101
|
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
|
75
102
|
"""Generate a random string of the given size using the given characters."""
|
|
76
103
|
return "".join(random.choice(chars) for _ in range(size))
|
|
@@ -148,21 +175,45 @@ def get_log_file():
|
|
|
148
175
|
|
|
149
176
|
|
|
150
177
|
def remove_virus_file_by_signature(file_path, signatures, output_file_path=None):
|
|
151
|
-
|
|
178
|
+
"""Remove virus content from a file by matching signatures.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
file_path (str): Path to the file to be cleaned.
|
|
182
|
+
signatures (list): List of signatures to match and remove.
|
|
183
|
+
output_file_path (str, optional): Path to the cleaned output file.
|
|
184
|
+
Defaults to None, which overwrites the input file.
|
|
185
|
+
"""
|
|
152
186
|
data = read_file(file_path)
|
|
153
187
|
if check_virus_by_signature(data, signatures):
|
|
154
|
-
logger.warning("%s: Infected by Malware!", file_path)
|
|
155
188
|
fixed_data = replace_content_by_signatures(data, signatures)
|
|
156
189
|
write_file(output_file_path or file_path, fixed_data)
|
|
157
190
|
|
|
158
191
|
|
|
159
192
|
def replace_content_by_signatures(content, signatures):
|
|
193
|
+
"""Replace content in a string that matches given signatures.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
content (str): The input content.
|
|
197
|
+
signatures (list): List of signatures to match and remove.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
str: The cleaned content.
|
|
201
|
+
"""
|
|
160
202
|
for signature in signatures:
|
|
161
203
|
content = re.sub(signature, "", content)
|
|
162
204
|
return content
|
|
163
205
|
|
|
164
206
|
|
|
165
207
|
def check_virus_file_by_signature(file_path, signatures=None):
|
|
208
|
+
"""Check if a file contains a virus by matching signatures.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
file_path (str): Path to the file to be checked.
|
|
212
|
+
signatures (list, optional): List of signatures to match. Defaults to None, which uses FILE_VIRUS_SIGNATURES.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
bool: True if a virus signature is found, False otherwise.
|
|
216
|
+
"""
|
|
166
217
|
signatures = signatures or FILE_VIRUS_SIGNATURES
|
|
167
218
|
try:
|
|
168
219
|
data = read_file(file_path)
|
|
@@ -174,8 +225,44 @@ def check_virus_file_by_signature(file_path, signatures=None):
|
|
|
174
225
|
|
|
175
226
|
|
|
176
227
|
def check_virus_by_signature(content, signatures=None):
|
|
228
|
+
"""Check if a content contains a virus by matching signatures.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
content (str): The input content.
|
|
232
|
+
signatures (list, optional): List of signatures to match. Defaults to None, which uses FILE_VIRUS_SIGNATURES.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
bool: True if a virus signature is found, False otherwise.
|
|
236
|
+
"""
|
|
177
237
|
signatures = signatures or FILE_VIRUS_SIGNATURES
|
|
178
238
|
for signature in signatures:
|
|
179
239
|
if re.search(signature, content):
|
|
180
240
|
return True
|
|
181
241
|
return False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def get_backup_path(path, root_path=None):
|
|
245
|
+
"""Get the backup path for a given file path based on environment variables.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
path (str): Path to the original file.
|
|
249
|
+
root_path (str, optional): Path to the root folder where backups should be saved.
|
|
250
|
+
Defaults to None, which saves backups in the original file's folder.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
str: The backup path.
|
|
254
|
+
"""
|
|
255
|
+
ignore_backup = os.getenv("MAYA_UMBRELLA_IGNORE_BACKUP", "false").lower() == "true"
|
|
256
|
+
if ignore_backup:
|
|
257
|
+
return path
|
|
258
|
+
root, filename = os.path.split(path)
|
|
259
|
+
backup_folder_name = os.getenv("MAYA_UMBRELLA_BACKUP_FOLDER_NAME", "_virus")
|
|
260
|
+
backup_path = os.path.join(root, backup_folder_name)
|
|
261
|
+
if root_path:
|
|
262
|
+
_, base_path = os.path.splitdrive(root)
|
|
263
|
+
backup_path = os.path.join(root_path, base_path.strip(os.sep))
|
|
264
|
+
try:
|
|
265
|
+
os.makedirs(backup_path)
|
|
266
|
+
except (OSError, IOError): # noqa: UP024
|
|
267
|
+
pass
|
|
268
|
+
return os.path.join(backup_path, filename)
|
maya_umbrella/i18n.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Import built-in modules
|
|
2
|
+
import glob
|
|
3
|
+
import os
|
|
4
|
+
from string import Template
|
|
5
|
+
|
|
6
|
+
# Import local modules
|
|
7
|
+
from maya_umbrella.filesystem import read_json
|
|
8
|
+
from maya_umbrella.filesystem import this_root
|
|
9
|
+
from maya_umbrella.maya_funs import maya_ui_language
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Translator(object):
|
|
13
|
+
"""A class to handle translations for different locales.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
data (dict): Dictionary containing translation data for different locales.
|
|
17
|
+
locale (str): The current locale.
|
|
18
|
+
"""
|
|
19
|
+
def __init__(self, file_format="json", default_locale=None):
|
|
20
|
+
"""Initialize the Translator.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
file_format (str, optional): File format of the translation files. Defaults to "json".
|
|
24
|
+
default_locale (str, optional): Default locale to use for translations. Defaults to None,
|
|
25
|
+
which uses the MAYA_UMBRELLA_LANG environment variable or the Maya UI language.
|
|
26
|
+
"""
|
|
27
|
+
_default_locale = os.getenv("MAYA_UMBRELLA_LANG", maya_ui_language())
|
|
28
|
+
default_locale = default_locale or _default_locale
|
|
29
|
+
self.data = {}
|
|
30
|
+
self.locale = default_locale
|
|
31
|
+
translations_folder = os.path.join(this_root(), "locales")
|
|
32
|
+
|
|
33
|
+
# get list of files with specific extensions
|
|
34
|
+
files = glob.glob(os.path.join(translations_folder, "*.{file_format}".format(file_format=file_format)))
|
|
35
|
+
for fil in files:
|
|
36
|
+
# get the name of the file without extension, will be used as locale name
|
|
37
|
+
loc = os.path.splitext(os.path.basename(fil))[0]
|
|
38
|
+
self.data[loc] = read_json(fil)
|
|
39
|
+
|
|
40
|
+
def set_locale(self, locale):
|
|
41
|
+
"""Set the current locale.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
locale (str): The locale to set.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If the provided locale is not supported.
|
|
48
|
+
"""
|
|
49
|
+
if locale in self.data:
|
|
50
|
+
self.locale = locale
|
|
51
|
+
else:
|
|
52
|
+
raise ValueError("Invalid locale: {loc}".format(loc=locale))
|
|
53
|
+
|
|
54
|
+
def get_locale(self):
|
|
55
|
+
"""Get the current locale.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
str: The current locale.
|
|
59
|
+
"""
|
|
60
|
+
return self.locale
|
|
61
|
+
|
|
62
|
+
def translate(self, key, **kwargs):
|
|
63
|
+
"""Translate a text based on the current locale.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
key (str): The key to be translated.
|
|
67
|
+
**kwargs: Arbitrary keyword arguments that are used to replace placeholders in the translation text.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
str: The translated text.
|
|
71
|
+
"""
|
|
72
|
+
# return the key instead of translation text if locale is not supported
|
|
73
|
+
if self.locale not in self.data:
|
|
74
|
+
return key
|
|
75
|
+
text = self.data[self.locale].get(key, key)
|
|
76
|
+
return Template(text).safe_substitute(**kwargs)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"start_fix_issues": "Start fixing all problems related to Maya virus $name",
|
|
3
|
+
"finish_fix_issues": "Done.",
|
|
4
|
+
"init_message": "Successfully loaded <hl>maya_umbrella</hl> under protection.",
|
|
5
|
+
"init_standalone_message": "-----------------------Loading maya_umbrella successfully----------------------",
|
|
6
|
+
"report_issue": "$name: Infected by Malware!",
|
|
7
|
+
"infected_nodes": "Infected nodes: $name: ",
|
|
8
|
+
"bad_files": "Bad files: $name",
|
|
9
|
+
"infected_script_jobs": "Infected script jobs: $name",
|
|
10
|
+
"infected_files": "Infected files: $name",
|
|
11
|
+
"infected_reference_files": "Infected reference files: $name",
|
|
12
|
+
"fix_infected_files": "Clean infected files: $name",
|
|
13
|
+
"fix_infected_nodes": "Delete infected nodes:$name",
|
|
14
|
+
"fix_infected_reference_nodes": "trying fix infected nodes from reference:$name",
|
|
15
|
+
"delete": "Deleting: $name",
|
|
16
|
+
"remove_file": "Deleting file:$name",
|
|
17
|
+
"remove_path": "Deleting path:$name",
|
|
18
|
+
"fix_script_job": "Kill script job: %s"
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"start_fix_issues": "开始修复与 Maya 病毒相关的所有问题 $name",
|
|
3
|
+
"finish_fix_issues": "修复结束.",
|
|
4
|
+
"init_message": "成功加载 <hl>maya_umbrella</hl> 保护中.",
|
|
5
|
+
"init_standalone_message": "-----------------------成功加载 maya_umbrella-----------------------",
|
|
6
|
+
"report_issue": "$name:被恶意感染!",
|
|
7
|
+
"infected_nodes": "被感染节点:$name: ",
|
|
8
|
+
"bad_files": "需要被清理的文件:$name",
|
|
9
|
+
"infected_script_jobs": "被感染的script jobs:$name",
|
|
10
|
+
"infected_files": "被感染的脚本:$name",
|
|
11
|
+
"infected_reference_files": "被感染的参考文件:$name",
|
|
12
|
+
"fix_infected_files": "清理被感染的文件:$name",
|
|
13
|
+
"fix_infected_nodes": "删除被感染的节点:$name",
|
|
14
|
+
"fix_infected_reference_nodes": "尝试修复被感染的参考节点:$name",
|
|
15
|
+
"delete": "删除感染文件:$name",
|
|
16
|
+
"remove_file": "删除文件:$name",
|
|
17
|
+
"remove_path": "删除文件夹:$name",
|
|
18
|
+
"fix_script_job": "删除被感染的节点:$name"
|
|
19
|
+
}
|
maya_umbrella/maya_funs.py
CHANGED
|
@@ -22,15 +22,115 @@ except ImportError:
|
|
|
22
22
|
om = MagicMock()
|
|
23
23
|
mel = MagicMock()
|
|
24
24
|
|
|
25
|
+
# Import built-in modules
|
|
26
|
+
from functools import wraps
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
def is_maya_standalone():
|
|
27
|
-
"""
|
|
30
|
+
"""Check if Maya is running in standalone mode.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
bool: True if Maya is running in standalone mode, False otherwise.
|
|
34
|
+
"""
|
|
28
35
|
return cmds.about(batch=True)
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
def check_reference_node_exists(node_name):
|
|
32
|
-
"""Check if reference node exists.
|
|
39
|
+
"""Check if a reference node exists in the Maya scene.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
node_name (str): Name of the reference node.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
bool: True if the reference node exists, False otherwise.
|
|
46
|
+
"""
|
|
33
47
|
try:
|
|
34
48
|
return cmds.referenceQuery(node_name, isNodeReferenced=True)
|
|
35
49
|
except RuntimeError:
|
|
36
50
|
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_reference_file_by_node(node_name):
|
|
54
|
+
"""Get the reference file associated with a node.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
node_name (str): Name of the node.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
str: Path of the reference file, empty string if the node is not associated with a reference file.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
return cmds.referenceQuery(node_name, filename=True)
|
|
64
|
+
except RuntimeError:
|
|
65
|
+
return ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_attr_value(node_name, attr_name):
|
|
69
|
+
"""Get the value of an attribute of a node.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
node_name (str): Name of the node.
|
|
73
|
+
attr_name (str): Name of the attribute.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Any: Value of the attribute, None if the attribute does not exist.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
return cmds.getAttr("{node_name}.{attr}".format(node_name=node_name, attr=attr_name))
|
|
80
|
+
except ValueError:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def maya_ui_language():
|
|
85
|
+
"""Get the language of the Maya user interface.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
str: The language of the Maya user interface.
|
|
89
|
+
"""
|
|
90
|
+
return cmds.about(uiLocaleLanguage=True)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def block_prompt(func):
|
|
94
|
+
"""Decorator to block file prompt dialogs in Maya.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
func (function): The function to decorate.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
function: The decorated function.
|
|
101
|
+
"""
|
|
102
|
+
@wraps(func)
|
|
103
|
+
def wrap(*args, **kwargs):
|
|
104
|
+
# Grabs the initial value.
|
|
105
|
+
prompt_val = cmds.file(prompt=True, q=True)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
cmds.file(prompt=False)
|
|
109
|
+
return func(*args, **kwargs)
|
|
110
|
+
|
|
111
|
+
finally:
|
|
112
|
+
# Resets to the original value, this way you don't suddenly turn the prompt on, when someone wanted it off.
|
|
113
|
+
cmds.file(prompt=prompt_val)
|
|
114
|
+
|
|
115
|
+
return wrap
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@block_prompt
|
|
119
|
+
def open_maya_file(maya_file):
|
|
120
|
+
"""Open a Maya file.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
maya_file (str): Path to the Maya file.
|
|
124
|
+
"""
|
|
125
|
+
cmds.file(maya_file, open=True, force=True, ignoreVersion=True, executeScriptNodes=False)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@block_prompt
|
|
129
|
+
def save_as_file(file_name):
|
|
130
|
+
"""Save the current Maya scene as a file.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
file_name (str): Path to the output file.
|
|
134
|
+
"""
|
|
135
|
+
cmds.file(rename=file_name)
|
|
136
|
+
cmds.file(s=True, f=True)
|
maya_umbrella/scanner.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Import built-in modules
|
|
2
|
+
import glob
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
# Import local modules
|
|
8
|
+
from maya_umbrella import maya_funs
|
|
9
|
+
from maya_umbrella.defender import context_defender
|
|
10
|
+
from maya_umbrella.filesystem import get_backup_path
|
|
11
|
+
from maya_umbrella.filesystem import read_file
|
|
12
|
+
from maya_umbrella.maya_funs import cmds
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MayaVirusScanner(object):
|
|
16
|
+
"""A class to scan and fix Maya files containing viruses.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
_failed_files (list): List of files that failed to be fixed.
|
|
20
|
+
_fixed_files (list): List of files that have been fixed.
|
|
21
|
+
logger (Logger): Logger object for logging purposes.
|
|
22
|
+
defender (MayaVirusDefender): MayaVirusDefender object for fixing issues.
|
|
23
|
+
_env (dict): Custom environment variables.
|
|
24
|
+
output_path (str, optional): Path to save the fixed files. Defaults to None, which overwrites the original
|
|
25
|
+
files.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, output_path=None, env=None):
|
|
29
|
+
"""Initialize the MayaVirusScanner.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
output_path (str, optional): Path to save the fixed files. Defaults to None, which overwrites the original
|
|
33
|
+
files.
|
|
34
|
+
env (dict, optional): Custom environment variables. Defaults to None,
|
|
35
|
+
which sets the 'MAYA_COLOR_MANAGEMENT_SYNCOLOR' variable to '1'.
|
|
36
|
+
"""
|
|
37
|
+
self.logger = logging.getLogger(__name__)
|
|
38
|
+
self.defender = None
|
|
39
|
+
self.output_path = output_path
|
|
40
|
+
self._failed_files = []
|
|
41
|
+
self._reference_files = []
|
|
42
|
+
self._fixed_files = []
|
|
43
|
+
# Custom env.
|
|
44
|
+
self._env = env or {
|
|
45
|
+
"MAYA_COLOR_MANAGEMENT_SYNCOLOR": "1"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def scan_files_from_pattern(self, pattern):
|
|
49
|
+
"""Scan and fix Maya files matching a given pattern.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
pattern (str): The file pattern to match.
|
|
53
|
+
"""
|
|
54
|
+
os.environ.update(self._env)
|
|
55
|
+
return self.scan_files_from_list(glob.iglob(pattern))
|
|
56
|
+
|
|
57
|
+
def scan_files_from_list(self, files):
|
|
58
|
+
"""Scan and fix Maya files from a given list.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
files (list): List of file paths to scan and fix.
|
|
62
|
+
"""
|
|
63
|
+
with context_defender() as defender:
|
|
64
|
+
self.defender = defender
|
|
65
|
+
for maya_file in files:
|
|
66
|
+
self._fix(maya_file)
|
|
67
|
+
while len(self._reference_files) > 0:
|
|
68
|
+
for ref in self._reference_files:
|
|
69
|
+
self._fix(ref)
|
|
70
|
+
return self._fixed_files
|
|
71
|
+
|
|
72
|
+
def scan_files_from_file(self, text_file):
|
|
73
|
+
"""Scan and fix Maya files from a given text file containing a list of file paths.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
text_file (str): Path to the text file containing the list of file paths.
|
|
77
|
+
"""
|
|
78
|
+
file_data = read_file(text_file)
|
|
79
|
+
files = file_data.splitlines()
|
|
80
|
+
return self.scan_files_from_list(files)
|
|
81
|
+
|
|
82
|
+
def _fix(self, maya_file):
|
|
83
|
+
"""Fix a single Maya file containing a virus.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
maya_file (str): Path to the Maya file to be fixed.
|
|
87
|
+
"""
|
|
88
|
+
if not maya_file and maya_file in self._fixed_files:
|
|
89
|
+
self.logger.debug("Already fixed: {maya_file}".format(maya_file=maya_file))
|
|
90
|
+
return
|
|
91
|
+
try:
|
|
92
|
+
maya_funs.open_maya_file(maya_file)
|
|
93
|
+
self.defender.collect()
|
|
94
|
+
except Exception:
|
|
95
|
+
self._failed_files.append(maya_file)
|
|
96
|
+
if self.defender.have_issues:
|
|
97
|
+
self.defender.fix()
|
|
98
|
+
backup_path = get_backup_path(maya_file, root_path=self.output_path)
|
|
99
|
+
self.logger.debug("Backup saved to: {backup_path}".format(backup_path=backup_path))
|
|
100
|
+
shutil.copy2(maya_file, backup_path)
|
|
101
|
+
cmds.file(s=True, f=True)
|
|
102
|
+
self._fixed_files.append(maya_file)
|
|
103
|
+
self._reference_files.extend(self.defender.collector.infected_reference_files)
|
|
104
|
+
if maya_file in self._reference_files:
|
|
105
|
+
self._reference_files.remove(maya_file)
|
|
106
|
+
cmds.file(new=True, force=True)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Import built-in modules
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
VirusSignature = namedtuple("VirusSignature", ["name", "signature"])
|
|
6
|
+
|
|
7
|
+
# https://regex101.com/r/0MNzF7/1
|
|
8
|
+
virus20240430_sig1 = VirusSignature("virus20240430", "python(.*);.+exec.+(pyCode).+;")
|
|
9
|
+
# https://regex101.com/r/2D14UA/1
|
|
10
|
+
virus20240430_sig2 = VirusSignature("virus20240430", r"^\['.+']")
|