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.
@@ -1,3 +1,5 @@
1
+ """Demo complex extractor."""
2
+
1
3
  from io import BytesIO
2
4
  from typing import List, Optional
3
5
 
@@ -39,6 +41,16 @@ class Complex(extractor.Extractor):
39
41
  """
40
42
 
41
43
  def run(self, stream: BytesIO, matches: List[yara.Match]) -> Optional[model.ExtractorModel]:
44
+ """Run the analysis process.
45
+
46
+ Args:
47
+ stream (BytesIO): file object from disk/network/memory.
48
+ matches (List[yara.Match]): yara rule matches
49
+
50
+ Returns:
51
+ (Optional[model.ExtractorModel]): model of results
52
+
53
+ """
42
54
  self.logger.info("starting run")
43
55
  self.logger.debug(f"{[x.rule for x in matches]=}")
44
56
  data = stream.read()
@@ -1,3 +1,12 @@
1
- def getdata():
2
- """This could be some complex and long function to support the main script."""
1
+ """Example of a complex function invoked by the extractor."""
2
+
3
+ from typing import Dict
4
+
5
+
6
+ def getdata() -> Dict[str, int]:
7
+ """This could be some complex and long function to support the main script.
8
+
9
+ Returns:
10
+ (Dict[str, int]): returns mock results
11
+ """
3
12
  return {"result": 5}
demo_extractors/elfy.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Demo extractor that targets ELF files."""
2
+
1
3
  from io import BytesIO
2
4
  from typing import List, Optional
3
5
 
@@ -21,6 +23,16 @@ class Elfy(extractor.Extractor):
21
23
  """
22
24
 
23
25
  def run(self, stream: BytesIO, matches: List[yara.Match]) -> Optional[model.ExtractorModel]:
26
+ """Run the analysis process.
27
+
28
+ Args:
29
+ stream (BytesIO): file object from disk/network/memory.
30
+ matches (List[yara.Match]): yara rule matches
31
+
32
+ Returns:
33
+ (Optional[model.ExtractorModel]): model of results
34
+
35
+ """
24
36
  # return config model formatted results
25
37
  ret = model.ExtractorModel(family=self.family)
26
38
  # the list for campaign_id already exists and is empty, so we just add an item
@@ -1,3 +1,5 @@
1
+ """Demo extractor to show the usage of the other field in the model."""
2
+
1
3
  from io import BytesIO
2
4
  from typing import List, Optional
3
5
 
@@ -23,6 +25,19 @@ class LimitOther(extractor.Extractor):
23
25
  """
24
26
 
25
27
  def run(self, stream: BytesIO, matches: List[yara.Match]) -> Optional[model.ExtractorModel]:
28
+ """Run the analysis process.
29
+
30
+ Args:
31
+ stream (BytesIO): file object from disk/network/memory.
32
+ matches (List[yara.Match]): yara rule matches
33
+
34
+ Returns:
35
+ (Optional[model.ExtractorModel]): model of results
36
+
37
+ Raises:
38
+ Exception: if the httpx library is not installed
39
+
40
+ """
26
41
  # import httpx at runtime so we can test that requirements.txt is installed dynamically without breaking
27
42
  # the tests that do direct importing
28
43
  import httpx
@@ -1,7 +1,9 @@
1
+ """Demo extractor that returns nothing."""
2
+
1
3
  from io import BytesIO
2
- from typing import List, Optional
4
+ from typing import List
3
5
 
4
- from maco import extractor, model, yara
6
+ from maco import extractor, yara
5
7
 
6
8
 
7
9
  class Nothing(extractor.Extractor):
@@ -21,6 +23,12 @@ class Nothing(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]):
27
+ """Run the analysis process.
28
+
29
+ Args:
30
+ stream (BytesIO): file object from disk/network/memory.
31
+ matches (List[yara.Match]): yara rule matches
32
+ """
25
33
  # return config model formatted results
26
34
  return
demo_extractors/shared.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Custom model based on Maco's model."""
2
+
1
3
  from typing import Optional
2
4
 
3
5
  import pydantic
@@ -6,7 +8,11 @@ from maco import model
6
8
 
7
9
 
8
10
  class MyCustomModel(model.ExtractorModel):
11
+ """Custom model based on Maco's model."""
12
+
9
13
  class Other(pydantic.BaseModel):
14
+ """Custom 'other' class."""
15
+
10
16
  key1: str
11
17
  key2: bool
12
18
  key3: int
@@ -1,3 +1,5 @@
1
+ """Example extractor that terminates early during extraction."""
2
+
1
3
  from io import BytesIO
2
4
  from typing import List, Optional
3
5
 
@@ -6,12 +8,21 @@ from maco.exceptions import AnalysisAbortedException
6
8
 
7
9
 
8
10
  class Terminator(extractor.Extractor):
9
- """Terminates early during extraction"""
11
+ """Terminates early during extraction."""
10
12
 
11
13
  family = "terminator"
12
14
  author = "skynet"
13
15
  last_modified = "1997-08-29"
14
16
 
15
17
  def run(self, stream: BytesIO, matches: List[yara.Match]) -> Optional[model.ExtractorModel]:
18
+ """Run the analysis process but terminate early.
19
+
20
+ Args:
21
+ stream (BytesIO): file object from disk/network/memory.
22
+ matches (List[yara.Match]): yara rule matches
23
+
24
+ Raises:
25
+ AnalysisAbortedException: Extractor has decided to terminate early
26
+ """
16
27
  # Terminate early and indicate I can't run on this sample
17
28
  raise AnalysisAbortedException("I can't run on this sample")
maco/base_test.py CHANGED
@@ -1,7 +1,6 @@
1
1
  """Foundation for unit testing an extractor.
2
2
 
3
3
  Example:
4
-
5
4
  from maco import base_test
6
5
  class TestExample(base_test.BaseTest):
7
6
  name = "Example"
@@ -20,13 +19,12 @@ import unittest
20
19
  import cart
21
20
 
22
21
  from maco import collector
23
-
24
-
25
- class NoHitException(Exception):
26
- pass
22
+ from maco.exceptions import NoHitException
27
23
 
28
24
 
29
25
  class BaseTest(unittest.TestCase):
26
+ """Base test class."""
27
+
30
28
  name: str = None # name of the extractor
31
29
  # folder and/or file where extractor is.
32
30
  # I recommend something like os.path.join(__file__, "../../extractors")
@@ -36,6 +34,11 @@ class BaseTest(unittest.TestCase):
36
34
 
37
35
  @classmethod
38
36
  def setUpClass(cls) -> None:
37
+ """Initialization of class.
38
+
39
+ Raises:
40
+ Exception: when name or path is not set.
41
+ """
39
42
  if not cls.name or not cls.path:
40
43
  raise Exception("name and path must be set")
41
44
  cls.c = collector.Collector(cls.path, include=[cls.name], create_venv=cls.create_venv)
@@ -47,7 +50,11 @@ class BaseTest(unittest.TestCase):
47
50
  self.assertEqual(len(self.c.extractors), 1)
48
51
 
49
52
  def extract(self, stream):
50
- """Return results for running extractor over stream, including yara check."""
53
+ """Return results for running extractor over stream, including yara check.
54
+
55
+ Raises:
56
+ NoHitException: when yara rule doesn't hit.
57
+ """
51
58
  runs = self.c.match(stream)
52
59
  if not runs:
53
60
  raise NoHitException("no yara rule hit")
@@ -65,7 +72,17 @@ class BaseTest(unittest.TestCase):
65
72
 
66
73
  @classmethod
67
74
  def load_cart(cls, filepath: str) -> io.BytesIO:
68
- """Load and unneuter a test file (likely malware) into memory for processing."""
75
+ """Load and unneuter a test file (likely malware) into memory for processing.
76
+
77
+ Args:
78
+ filepath (str): Path to carted sample
79
+
80
+ Returns:
81
+ (io.BytesIO): Buffered stream containing the un-carted sample
82
+
83
+ Raises:
84
+ FileNotFoundError: if the path to the sample doesn't exist
85
+ """
69
86
  # it is nice if we can load files relative to whatever is implementing base_test
70
87
  dirpath = os.path.split(cls._get_location())[0]
71
88
  # either filepath is absolute, or should be loaded relative to child of base_test
maco/cli.py CHANGED
@@ -3,19 +3,18 @@
3
3
  import argparse
4
4
  import base64
5
5
  import binascii
6
- import cart
7
6
  import hashlib
8
7
  import io
9
8
  import json
10
9
  import logging
11
10
  import os
12
11
  import sys
13
-
14
12
  from importlib.metadata import version
15
13
  from typing import BinaryIO, List, Tuple
16
14
 
17
- from maco import collector
15
+ import cart
18
16
 
17
+ from maco import collector
19
18
 
20
19
  logger = logging.getLogger("maco.lib.cli")
21
20
 
@@ -29,7 +28,20 @@ def process_file(
29
28
  force: bool,
30
29
  include_base64: bool,
31
30
  ):
32
- """Process a filestream with the extractors and rules."""
31
+ """Process a filestream with the extractors and rules.
32
+
33
+ Args:
34
+ collected (collector.Collector): a Collector instance
35
+ path_file (str): path to sample to be analyzed
36
+ stream (BinaryIO): binary stream to be analyzed
37
+ pretty (bool): Pretty print the JSON output
38
+ force (bool): Run all extractors regardless of YARA rule match
39
+ include_base64 (bool): include base64'd data in output
40
+
41
+ Returns:
42
+ (dict): The output from the extractors analyzing the sample
43
+
44
+ """
33
45
  unneutered = io.BytesIO()
34
46
  try:
35
47
  cart.unpack_stream(stream, unneutered)
@@ -98,7 +110,8 @@ def process_filesystem(
98
110
  ) -> Tuple[int, int, int]:
99
111
  """Process filesystem with extractors and print results of extraction.
100
112
 
101
- Returns total number of analysed files, yara hits and successful maco extractions.
113
+ Returns:
114
+ (Tuple[int, int, int]): Total number of analysed files, yara hits and successful maco extractions.
102
115
  """
103
116
  if force:
104
117
  logger.warning("force execute will cause errors if an extractor requires a yara rule hit during execution")
@@ -163,6 +176,7 @@ def process_filesystem(
163
176
 
164
177
 
165
178
  def main():
179
+ """Main block for CLI."""
166
180
  parser = argparse.ArgumentParser(description="Run extractors over samples.")
167
181
  parser.add_argument("extractors", type=str, help="path to extractors")
168
182
  parser.add_argument("samples", type=str, help="path to samples")
maco/collector.py CHANGED
@@ -13,18 +13,20 @@ from multiprocess import Manager, Process, Queue
13
13
  from pydantic import BaseModel
14
14
 
15
15
  from maco import extractor, model, utils, yara
16
- from maco.exceptions import AnalysisAbortedException
17
-
18
-
19
- class ExtractorLoadError(Exception):
20
- pass
21
-
16
+ from maco.exceptions import AnalysisAbortedException, ExtractorLoadError
22
17
 
23
18
  logger = logging.getLogger("maco.lib.helpers")
24
19
 
25
20
 
26
21
  def _verify_response(resp: Union[BaseModel, dict]) -> Dict:
27
- """Enforce types and verify properties, and remove defaults."""
22
+ """Enforce types and verify properties, and remove defaults.
23
+
24
+ Args:
25
+ resp (Union[BaseModel, dict])): results from extractor
26
+
27
+ Returns:
28
+ (Dict): results from extractor after verification
29
+ """
28
30
  if not resp:
29
31
  return None
30
32
  # check the response is valid for its own model
@@ -62,6 +64,8 @@ class ExtractorRegistration(TypedDict):
62
64
 
63
65
 
64
66
  class Collector:
67
+ """Discover and load extractors from file system."""
68
+
65
69
  def __init__(
66
70
  self,
67
71
  path_extractors: str,
@@ -70,7 +74,11 @@ class Collector:
70
74
  create_venv: bool = False,
71
75
  skip_install: bool = False,
72
76
  ):
73
- """Discover and load extractors from file system."""
77
+ """Discover and load extractors from file system.
78
+
79
+ Raises:
80
+ ExtractorLoadError: when no extractors are found
81
+ """
74
82
  # maco requires the extractor to be imported directly, so ensure they are available on the path
75
83
  full_path_extractors = os.path.abspath(path_extractors)
76
84
  full_path_above_extractors = os.path.dirname(full_path_extractors)
@@ -175,7 +183,15 @@ class Collector:
175
183
  stream: BinaryIO,
176
184
  extractor_name: str,
177
185
  ) -> Dict[str, Any]:
178
- """Run extractor with stream and verify output matches the model."""
186
+ """Run extractor with stream and verify output matches the model.
187
+
188
+ Args:
189
+ stream (BinaryIO): Binary stream to analyze
190
+ extractor_name (str): Name of extractor to analyze stream
191
+
192
+ Returns:
193
+ (Dict[str, Any]): Results from extractor
194
+ """
179
195
  extractor = self.extractors[extractor_name]
180
196
  try:
181
197
  # Run extractor on a copy of the sample
maco/exceptions.py CHANGED
@@ -1,3 +1,33 @@
1
+ """Exception classes for extractors."""
2
+
3
+
1
4
  # Can be raised by extractors to abort analysis of a sample
2
5
  # ie. Can abort if preliminary checks at start of run indicate the file shouldn't be analyzed by extractor
3
- class AnalysisAbortedException(Exception): ...
6
+ class AnalysisAbortedException(Exception):
7
+ """Raised when extractors voluntarily abort analysis of a sample."""
8
+
9
+ pass
10
+
11
+
12
+ class ExtractorLoadError(Exception):
13
+ """Raised when extractors cannot be loaded."""
14
+
15
+ pass
16
+
17
+
18
+ class InvalidExtractor(ValueError):
19
+ """Raised when an extractor is invalid."""
20
+
21
+ pass
22
+
23
+
24
+ class NoHitException(Exception):
25
+ """Raised when the YARA rule of an extractor doesn't hit."""
26
+
27
+ pass
28
+
29
+
30
+ class SyntaxError(Exception):
31
+ """Raised when there's a syntax error in the YARA rule."""
32
+
33
+ pass
maco/extractor.py CHANGED
@@ -4,14 +4,8 @@ import logging
4
4
  import textwrap
5
5
  from typing import BinaryIO, List, Optional, Union
6
6
 
7
- from maco import yara
8
-
9
- from . import model
10
-
11
-
12
- class InvalidExtractor(ValueError):
13
- pass
14
-
7
+ from maco import model, yara
8
+ from maco.exceptions import InvalidExtractor
15
9
 
16
10
  DEFAULT_YARA_RULE = """
17
11
  rule {name}
@@ -37,6 +31,11 @@ class Extractor:
37
31
  logger: logging.Logger = None # logger for use when debugging
38
32
 
39
33
  def __init__(self) -> None:
34
+ """Initialise the extractor.
35
+
36
+ Raises:
37
+ InvalidExtractor: When the extractor is invalid.
38
+ """
40
39
  self.name = name = type(self).__name__
41
40
  self.logger = logging.getLogger(f"maco.extractor.{name}")
42
41
  self.logger.debug(f"initialise '{name}'")
maco/model/model.py CHANGED
@@ -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