maco 1.2.14__py3-none-any.whl → 1.2.15__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.
@@ -29,6 +29,8 @@ class Encryption(ForbidModel):
29
29
  """Encryption usage."""
30
30
 
31
31
  class UsageEnum(str, Enum):
32
+ """Purpose of the encryption."""
33
+
32
34
  config = "config"
33
35
  communication = "communication"
34
36
  binary = "binary"
@@ -52,6 +54,8 @@ class Encryption(ForbidModel):
52
54
 
53
55
 
54
56
  class CategoryEnum(str, Enum):
57
+ """Category of the malware."""
58
+
55
59
  # Software that shows you extra promotions that you cannot control as you use your PC.
56
60
  # You wouldn't see the extra ads if you didn't have adware installed.
57
61
  adware = "adware"
@@ -274,6 +278,8 @@ class ExtractorModel(ForbidModel):
274
278
  """Binary data extracted by decoder."""
275
279
 
276
280
  class TypeEnum(str, Enum):
281
+ """Type of binary data."""
282
+
277
283
  payload = "payload" # contained within the original file
278
284
  config = "config" # sometimes malware uses json/formatted text for config
279
285
  other = "other"
@@ -289,6 +295,8 @@ class ExtractorModel(ForbidModel):
289
295
  # convenience for ret.encryption.append(ret.Encryption(*properties))
290
296
  # Define as class as only way to allow for this to be accessed and not have pydantic try to parse it.
291
297
  class Encryption(Encryption):
298
+ """Encryption usage."""
299
+
292
300
  pass
293
301
 
294
302
  encryption: Union[List[Encryption], Encryption, None] = None # encryption information for the binary
@@ -436,6 +444,8 @@ class ExtractorModel(ForbidModel):
436
444
  """Direct usage of DNS."""
437
445
 
438
446
  class RecordTypeEnum(str, Enum):
447
+ """DNS record types."""
448
+
439
449
  A = "A"
440
450
  AAAA = "AAAA"
441
451
  AFSDB = "AFSDB"
@@ -512,6 +522,8 @@ class ExtractorModel(ForbidModel):
512
522
  # convenience for ret.encryption.append(ret.Encryption(*properties))
513
523
  # Define as class as only way to allow for this to be accessed and not have pydantic try to parse it.
514
524
  class Encryption(Encryption):
525
+ """Encryption usage."""
526
+
515
527
  pass
516
528
 
517
529
  encryption: List[Encryption] = []
@@ -530,6 +542,8 @@ class ExtractorModel(ForbidModel):
530
542
  """Cryptocoin usage (ransomware/miner)."""
531
543
 
532
544
  class UsageEnum(str, Enum):
545
+ """Cryptocoin usage."""
546
+
533
547
  ransomware = "ransomware" # request money to unlock
534
548
  miner = "miner" # use gpu/cpu to mint coins
535
549
  other = "other"
@@ -543,7 +557,11 @@ class ExtractorModel(ForbidModel):
543
557
  cryptocurrency: List[Cryptocurrency] = []
544
558
 
545
559
  class Path(ForbidModel):
560
+ """Path used by malware."""
561
+
546
562
  class UsageEnum(str, Enum):
563
+ """Purpose of the path."""
564
+
547
565
  c2 = "c2" # file/folder issues commands to malware
548
566
  config = "config" # config is loaded from this path
549
567
  install = "install" # install directory/filename for malware
@@ -559,7 +577,11 @@ class ExtractorModel(ForbidModel):
559
577
  paths: List[Path] = [] # files/directories used by malware
560
578
 
561
579
  class Registry(ForbidModel):
580
+ """Registry usage by malware."""
581
+
562
582
  class UsageEnum(str, Enum):
583
+ """Registry usage."""
584
+
563
585
  persistence = "persistence" # stay alive
564
586
  store_data = "store_data" # generated encryption keys or config
565
587
  store_payload = "store_payload" # malware hidden in registry key
model_setup/maco/utils.py CHANGED
@@ -1,4 +1,5 @@
1
- # Common utilities shared between the MACO collector and configextractor-py
1
+ """Common utilities shared between the MACO collector and configextractor-py."""
2
+
2
3
  import importlib
3
4
  import importlib.machinery
4
5
  import importlib.util
@@ -33,8 +34,8 @@ from typing import Callable, Dict, List, Set, Tuple, Union
33
34
  from uv import find_uv_bin
34
35
 
35
36
  from maco import model
36
- from maco.extractor import Extractor
37
37
  from maco.exceptions import AnalysisAbortedException
38
+ from maco.extractor import Extractor
38
39
 
39
40
  logger = logging.getLogger("maco.lib.utils")
40
41
 
@@ -50,10 +51,14 @@ VENV_CREATE_CMD = f"{UV_BIN} venv"
50
51
 
51
52
 
52
53
  class Base64Decoder(json.JSONDecoder):
54
+ """JSON decoder that also base64 encodes binary data."""
55
+
53
56
  def __init__(self, *args, **kwargs):
57
+ """Initialize the decoder."""
54
58
  json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
55
59
 
56
60
  def object_hook(self, obj):
61
+ """Hook to decode base64 encoded binary data.""" # noqa: DOC201
57
62
  if "__class__" not in obj:
58
63
  return obj
59
64
  type = obj["__class__"]
@@ -131,17 +136,38 @@ rule MACO {
131
136
 
132
137
 
133
138
  def maco_extractor_validation(module: ModuleType) -> bool:
139
+ """Validation function for extractors.
140
+
141
+ Returns:
142
+ (bool): True if extractor belongs to MACO, False otherwise.
143
+ """
134
144
  if inspect.isclass(module):
135
145
  # 'author' has to be implemented otherwise will raise an exception according to MACO
136
146
  return hasattr(module, "author") and module.author
137
147
  return False
138
148
 
139
149
 
140
- def maco_extract_rules(module: Extractor) -> bool:
150
+ def maco_extract_rules(module: Extractor) -> str:
151
+ """Extracts YARA rules from extractor.
152
+
153
+ Returns:
154
+ (str): YARA rules
155
+ """
141
156
  return module.yara_rule
142
157
 
143
158
 
144
159
  def scan_for_extractors(root_directory: str, scanner: yara.Rules, logger: Logger) -> Tuple[List[str], List[str]]:
160
+ """Looks for extractors using YARA rules.
161
+
162
+ Args:
163
+ root_directory (str): Root directory containing extractors
164
+ scanner (yara.Rules): Scanner to look for extractors using YARA rules
165
+ logger (Logger): Logger to use
166
+
167
+ Returns:
168
+ Tuple[List[str], List[str]]: Returns a list of extractor directories and extractor files
169
+
170
+ """
145
171
  extractor_dirs = set([root_directory])
146
172
  extractor_files = []
147
173
 
@@ -177,17 +203,22 @@ def scan_for_extractors(root_directory: str, scanner: yara.Rules, logger: Logger
177
203
  with open(path, "rb") as f:
178
204
  data = f.read()
179
205
 
180
- with open(path, "wb") as f:
181
- # Replace any relative importing with absolute
182
- curr_dir = os.path.dirname(path)
183
- split = curr_dir.split("/")[::-1]
184
- for pattern in [RELATIVE_FROM_IMPORT_RE, RELATIVE_FROM_RE]:
185
- for match in pattern.findall(data):
186
- depth = match.count(b".")
187
- abspath = ".".join(split[depth - 1 : split.index(package) + 1][::-1])
188
- abspath += "." if pattern == RELATIVE_FROM_RE else ""
189
- data = data.replace(f"from {match.decode()}".encode(), f"from {abspath}".encode(), 1)
190
- f.write(data)
206
+ # Replace any relative importing with absolute
207
+ changed_imports = False
208
+ curr_dir = os.path.dirname(path)
209
+ split = curr_dir.split("/")[::-1]
210
+ for pattern in [RELATIVE_FROM_IMPORT_RE, RELATIVE_FROM_RE]:
211
+ for match in pattern.findall(data):
212
+ depth = match.count(b".")
213
+ abspath = ".".join(split[depth - 1 : split.index(package) + 1][::-1])
214
+ abspath += "." if pattern == RELATIVE_FROM_RE else ""
215
+ data = data.replace(f"from {match.decode()}".encode(), f"from {abspath}".encode(), 1)
216
+ changed_imports = True
217
+
218
+ # only write extractor files if imports were changed
219
+ if changed_imports:
220
+ with open(path, "wb") as f:
221
+ f.write(data)
191
222
 
192
223
  if scanner.match(path):
193
224
  # Add directory to list of hits for venv creation
@@ -282,7 +313,16 @@ def _install_required_packages(create_venv: bool, directories: List[str], python
282
313
  return venvs
283
314
 
284
315
 
285
- def find_and_insert_venv(path: str, venvs: List[str]):
316
+ def find_and_insert_venv(path: str, venvs: List[str]) -> Tuple[str, str]:
317
+ """Finds the closest virtual environment to the extractor and inserts it into the PATH.
318
+
319
+ Args:
320
+ path (str): Path of extractor
321
+ venvs (List[str]): List of virtual environments
322
+
323
+ Returns:
324
+ (Tuple[str, str]): Virtual environment and site-packages path that's closest to the extractor
325
+ """
286
326
  venv = None
287
327
  for venv in sorted(venvs, reverse=True):
288
328
  venv_parent = os.path.dirname(venv)
@@ -311,6 +351,16 @@ def register_extractors(
311
351
  logger: Logger,
312
352
  default_loaded_modules: Set[str] = set(sys.modules.keys()),
313
353
  ):
354
+ """Register extractors with in the current directory.
355
+
356
+ Args:
357
+ current_directory (str): Current directory to register extractors found
358
+ venvs (List[str]): List of virtual environments
359
+ extractor_files (List[str]): List of extractor files found
360
+ extractor_module_callback (Callable[[ModuleType, str], None]): Callback used to register extractors
361
+ logger (Logger): Logger to use
362
+ default_loaded_modules (Set[str]): Set of default loaded modules
363
+ """
314
364
  package_name = os.path.basename(current_directory)
315
365
  parent_directory = os.path.dirname(current_directory)
316
366
  if venvs and package_name in sys.modules:
@@ -413,6 +463,17 @@ def import_extractors(
413
463
  python_version: str = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
414
464
  skip_install: bool = False,
415
465
  ):
466
+ """Import extractors in a given directory.
467
+
468
+ Args:
469
+ extractor_module_callback (Callable[[ModuleType, str], bool]): Callback used to register extractors
470
+ root_directory (str): Root directory to look for extractors
471
+ scanner (yara.Rules): Scanner to look for extractors that match YARA rule
472
+ create_venv (bool): Create/Use virtual environments
473
+ logger (Logger): Logger to use
474
+ python_version (str): Version of python to use when creating virtual environments
475
+ skip_install (bool): Skip installation of Python dependencies for extractors
476
+ """
416
477
  extractor_dirs, extractor_files = scan_for_extractors(root_directory, scanner, logger)
417
478
 
418
479
  logger.info(f"Extractor files found based on scanner ({len(extractor_files)}).")
@@ -448,7 +509,24 @@ def run_extractor(
448
509
  venv_script=VENV_SCRIPT,
449
510
  json_decoder=Base64Decoder,
450
511
  ) -> Union[Dict[str, dict], model.ExtractorModel]:
451
- """Runs the maco extractor against sample either in current process or child process."""
512
+ """Runs the maco extractor against sample either in current process or child process.
513
+
514
+ Args:
515
+ sample_path (str): Path to sample
516
+ module_name (str): Name of extractor module
517
+ extractor_class (str): Name of extractor class in module
518
+ module_path (str): Path to Python module containing extractor
519
+ venv (str): Path to virtual environment associated to extractor
520
+ venv_script (str): Script to run extractor in a virtual environment
521
+ json_decoder (Base64Decoder): Decoder used for JSON
522
+
523
+ Raises:
524
+ AnalysisAbortedException: Raised when extractor voluntarily terminates execution
525
+ Exception: Raised when extractor raises an exception
526
+
527
+ Returns:
528
+ Union[Dict[str, dict], model.ExtractorModel]: Results from extractor
529
+ """
452
530
  if not venv:
453
531
  key = f"{module_name}_{extractor_class}"
454
532
  if key not in _loaded_extractors:
model_setup/maco/yara.py CHANGED
@@ -1,25 +1,34 @@
1
+ """yara-python facade that uses yara-x."""
2
+
1
3
  import re
2
4
  from collections import namedtuple
3
5
  from itertools import cycle
4
- from typing import Dict
6
+ from typing import Dict, List
5
7
 
6
8
  import yara_x
7
9
 
8
- RULE_ID_RE = re.compile("(\w+)? ?rule (\w+)")
9
-
10
+ from maco.exceptions import SyntaxError
10
11
 
11
- class SyntaxError(Exception): ...
12
+ RULE_ID_RE = re.compile("(\w+)? ?rule (\w+)")
12
13
 
13
14
 
14
15
  # Create interfaces that resembles yara-python (but is running yara-x under the hood)
15
16
  class StringMatchInstance:
17
+ """Instance of a string match."""
18
+
16
19
  def __init__(self, match: yara_x.Match, file_content: bytes):
20
+ """Initializes StringMatchInstance."""
17
21
  self.matched_data = file_content[match.offset : match.offset + match.length]
18
22
  self.matched_length = match.length
19
23
  self.offset = match.offset
20
24
  self.xor_key = match.xor_key
21
25
 
22
26
  def plaintext(self) -> bytes:
27
+ """Plaintext of the matched data.
28
+
29
+ Returns:
30
+ (bytes): Plaintext of the matched cipher text
31
+ """
23
32
  if not self.xor_key:
24
33
  # No need to XOR the matched data
25
34
  return self.matched_data
@@ -28,17 +37,28 @@ class StringMatchInstance:
28
37
 
29
38
 
30
39
  class StringMatch:
40
+ """String match."""
41
+
31
42
  def __init__(self, pattern: yara_x.Pattern, file_content: bytes):
43
+ """Initializes StringMatch."""
32
44
  self.identifier = pattern.identifier
33
45
  self.instances = [StringMatchInstance(match, file_content) for match in pattern.matches]
34
46
  self._is_xor = any([match.xor_key for match in pattern.matches])
35
47
 
36
48
  def is_xor(self):
49
+ """Checks if string match is xor'd.
50
+
51
+ Returns:
52
+ (bool): True if match is xor'd
53
+ """
37
54
  return self._is_xor
38
55
 
39
56
 
40
57
  class Match:
58
+ """Match."""
59
+
41
60
  def __init__(self, rule: yara_x.Rule, file_content: bytes):
61
+ """Initializes Match."""
42
62
  self.rule = rule.identifier
43
63
  self.namespace = rule.namespace
44
64
  self.tags = list(rule.tags) or []
@@ -50,7 +70,14 @@ class Match:
50
70
 
51
71
 
52
72
  class Rules:
73
+ """Rules."""
74
+
53
75
  def __init__(self, source: str = None, sources: Dict[str, str] = None):
76
+ """Initializes Rules.
77
+
78
+ Raises:
79
+ SyntaxError: Raised when there's a syntax error in the YARA rule.
80
+ """
54
81
  Rule = namedtuple("Rule", "identifier namespace is_global")
55
82
  if source:
56
83
  sources = {"default": source}
@@ -69,10 +96,20 @@ class Rules:
69
96
  raise SyntaxError(e)
70
97
 
71
98
  def __iter__(self):
99
+ """Iterate over rules.
100
+
101
+ Yields:
102
+ YARA rules
103
+ """
72
104
  for rule in self._rules:
73
105
  yield rule
74
106
 
75
- def match(self, filepath: str = None, data: bytes = None):
107
+ def match(self, filepath: str = None, data: bytes = None) -> List[Match]:
108
+ """Performs a scan to check for YARA rules matches based on the file, either given by path or buffer.
109
+
110
+ Returns:
111
+ (List[Match]): A list of YARA matches.
112
+ """
76
113
  if filepath:
77
114
  with open(filepath, "rb") as fp:
78
115
  data = fp.read()
@@ -81,4 +118,9 @@ class Rules:
81
118
 
82
119
 
83
120
  def compile(source: str = None, sources: Dict[str, str] = None) -> Rules:
121
+ """Compiles YARA rules from source or from sources.
122
+
123
+ Returns:
124
+ (Rules): a Rules object
125
+ """
84
126
  return Rules(source, sources)
tests/extractors/basic.py CHANGED
@@ -1,5 +1,7 @@
1
+ """Basic extractor."""
2
+
1
3
  from io import BytesIO
2
- from typing import List, Optional
4
+ from typing import List
3
5
 
4
6
  from maco import extractor, model, yara
5
7
 
@@ -21,7 +23,13 @@ class Basic(extractor.Extractor):
21
23
  }
22
24
  """
23
25
 
24
- def run(self, stream: BytesIO, matches: List[yara.Match]) -> Optional[model.ExtractorModel]:
26
+ def run(self, stream: BytesIO, matches: List[yara.Match]) -> model.ExtractorModel:
27
+ """Run the extractor.
28
+
29
+ Returns:
30
+ (model.ExtractorModel): Results from extractor
31
+
32
+ """
25
33
  # use a custom model that inherits from ExtractorModel
26
34
  # this model defines what can go in the 'other' dict
27
35
  tmp = model.ExtractorModel(family="basic")
@@ -1,5 +1,7 @@
1
+ """Basic longer extractor."""
2
+
1
3
  from io import BytesIO
2
- from typing import List, Optional
4
+ from typing import List
3
5
 
4
6
  from maco import extractor, model, yara
5
7
 
@@ -21,7 +23,12 @@ class BasicLonger(extractor.Extractor):
21
23
  }
22
24
  """
23
25
 
24
- def run(self, stream: BytesIO, matches: List[yara.Match]) -> Optional[model.ExtractorModel]:
26
+ def run(self, stream: BytesIO, matches: List[yara.Match]) -> model.ExtractorModel:
27
+ """Run the extractor.
28
+
29
+ Returns:
30
+ (model.ExtractorModel): Results from extractor
31
+ """
25
32
  # use a custom model that inherits from ExtractorModel
26
33
  # this model defines what can go in the 'other' dict
27
34
  tmp = model.ExtractorModel(family="basic_longer")
@@ -1,3 +1,5 @@
1
+ """Simple extractor for testing module and submodule with the same name."""
2
+
1
3
  from maco import extractor
2
4
 
3
5
 
File without changes