configextractor-py 1.0.dev0__tar.gz → 1.1.0__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.
Files changed (33) hide show
  1. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/.vscode/settings.json +4 -0
  2. {configextractor-py-1.0.dev0/configextractor_py.egg-info → configextractor-py-1.1.0}/PKG-INFO +1 -1
  3. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/base.py +13 -31
  4. configextractor-py-1.1.0/configextractor/frameworks/cape.py +61 -0
  5. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/maco.py +31 -26
  6. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/mwcp.py +40 -36
  7. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/main.py +11 -12
  8. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0/configextractor_py.egg-info}/PKG-INFO +1 -1
  9. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/SOURCES.txt +8 -1
  10. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/pipelines/publish.yaml +0 -10
  11. configextractor-py-1.1.0/pipelines/test.yaml +40 -0
  12. configextractor-py-1.1.0/tests/requirements.txt +1 -0
  13. configextractor-py-1.1.0/tests/test_detection.py +32 -0
  14. configextractor-py-1.1.0/tests/test_parsers/cape.py +11 -0
  15. configextractor-py-1.1.0/tests/test_parsers/maco.py +6 -0
  16. configextractor-py-1.1.0/tests/test_parsers/mwcp.py +6 -0
  17. configextractor-py-1.1.0/tests/test_run.py +15 -0
  18. configextractor-py-1.0.dev0/configextractor/frameworks/cape.py +0 -53
  19. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/.gitignore +0 -0
  20. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/.gitmodules +0 -0
  21. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/README.md +0 -0
  22. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/__init__.py +0 -0
  23. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/cli.py +0 -0
  24. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/__init__.py +0 -0
  25. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/malduck.py +0 -0
  26. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/ratdecoder.py.bak +0 -0
  27. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/dependency_links.txt +0 -0
  28. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/entry_points.txt +0 -0
  29. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/requires.txt +0 -0
  30. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/top_level.txt +0 -0
  31. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/requirements.txt +0 -0
  32. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/setup.cfg +0 -0
  33. {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/setup.py +0 -0
@@ -29,4 +29,8 @@
29
29
  //Added the ignore of E203 for now : https://github.com/PyCQA/pycodestyle/issues/373
30
30
  "--ignore=E203,W503"
31
31
  ],
32
+ "python.testing.pytestArgs": [
33
+ "tests"
34
+ ],
35
+ "python.testing.pytestEnabled": true,
32
36
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: configextractor-py
3
- Version: 1.0.dev0
3
+ Version: 1.1.0
4
4
  Summary: A library for extracting malware configurations across multiple frameworks
5
5
  Description-Content-Type: text/markdown
6
6
  Requires-Dist: cart
@@ -11,7 +11,8 @@ from plyara.utils import rebuild_yara_rule
11
11
 
12
12
 
13
13
  class Extractor:
14
- def __init__(self, framework, module, module_path, root_directory, yara_rule, venv=None) -> None:
14
+ def __init__(self, id, framework, module, module_path, root_directory, yara_rule, venv=None) -> None:
15
+ self.id = id
15
16
  self.framework = framework
16
17
  self.module = module
17
18
  self.module_path = module_path
@@ -36,45 +37,26 @@ class Framework:
36
37
  def get_name(extractor: Extractor):
37
38
  return extractor.module.__name__.split(".")[-1]
38
39
 
40
+ # Define a template for results from this Extractor
41
+ def result_template(self, extractor: Extractor, yara_matches: List[yara.Match]) -> Dict[str, str]:
42
+ return dict(id=extractor.id, yara_hits=[y.rule for y in yara_matches])
43
+
39
44
  # Extract YARA rules from module
40
- def extract_yara_from_module(self, decoder: object, module_name: str, existing_rule_names=[]) -> List[str]:
45
+ def extract_yara_from_module(self, decoder: object) -> List[str]:
41
46
  if self.yara_attr_name and hasattr(decoder, self.yara_attr_name) and getattr(decoder, self.yara_attr_name):
42
- yara_rules = list()
43
- # Modify YARA rule to include meta about the parser
44
47
  yara_parser = plyara.Plyara()
45
- for yara_rule_frag in yara_parser.parse_string(getattr(decoder, self.yara_attr_name)):
46
- # If this rule came with no metadata then instantiate it
47
- if not yara_rule_frag.get("metadata"):
48
- yara_rule_frag["metadata"] = list()
49
- yara_rule_name = yara_rule_frag["rule_name"]
50
- yara_rule_frag["metadata"].extend(
51
- [
52
- {"yara_identifier": yara_rule_name},
53
- {"parser_module": module_name},
54
- ]
55
- )
56
-
57
- # Modify the name of the rule to avoid duplicate identifiers during compilation
58
- if yara_rule_name in existing_rule_names:
59
- yara_rule_frag[
60
- "rule_name"
61
- ] = f"{yara_rule_name}_{len([i for i in existing_rule_names if i.startswith(yara_rule_name)])}"
62
-
63
- existing_rule_names.append(yara_rule_name)
64
- rebuilt_rule = rebuild_yara_rule(yara_rule_frag)
65
- try:
66
- yara.compile(source=rebuilt_rule)
67
- yara_rules.append(rebuilt_rule)
68
- except Exception as e:
69
- self.log.error(f"{decoder.__name__}: {e}")
70
- return yara_rules
48
+ return [
49
+ rebuild_yara_rule(yara_rule_frag)
50
+ for yara_rule_frag in yara_parser.parse_string(getattr(decoder, self.yara_attr_name))
51
+ ]
52
+ return []
71
53
 
72
54
  # Validate module against framework
73
55
  def validate(self, module: Any) -> bool:
74
56
  NotImplementedError()
75
57
 
76
58
  # Run a series of modules
77
- def run(self, sample_path: str, parsers: Dict[Extractor, List[yara.Match]]) -> Dict[str, dict]:
59
+ def run(self, sample_path: str, parsers: Dict[Extractor, List[yara.Match]]) -> List[dict]:
78
60
  return NotImplementedError()
79
61
 
80
62
  def run_in_venv(self, sample_path: str, extractor: Extractor) -> Dict[str, dict]:
@@ -0,0 +1,61 @@
1
+ # CAPE framework
2
+ from inspect import signature
3
+ from typing import Any, Dict, List
4
+
5
+ import yara
6
+ from maco.model import ExtractorModel
7
+
8
+ from configextractor.frameworks.base import Extractor, Framework
9
+
10
+
11
+ class CAPE(Framework):
12
+ @staticmethod
13
+ def get_classification(extractor: Extractor):
14
+ if hasattr(extractor.module, "TLP"):
15
+ return extractor.module.TLP
16
+ return None
17
+
18
+ def validate(self, module: Any) -> bool:
19
+ if hasattr(module, "extract_config"):
20
+ s = signature(module.extract_config)
21
+ # Ensure function isn't part of a class, which doesn't follow CAPE's extractor format
22
+ return len(s.parameters) == 1 and "self" not in s.parameters
23
+
24
+ def result_template(self, extractor: Extractor, yara_matches: List[yara.Match]) -> Dict[str, str]:
25
+ template = super().result_template(extractor, yara_matches)
26
+ template.update(
27
+ {
28
+ "author": extractor.module.AUTHOR if hasattr(extractor.module, "AUTHOR") else "<MISSING_AUTHOR>",
29
+ "description": extractor.module.DESCRIPTION
30
+ if hasattr(extractor.module, "DESCRIPTION")
31
+ else "<MISSING_DESCRIPTION>",
32
+ }
33
+ )
34
+
35
+ return template
36
+
37
+ def run(self, sample_path: str, parsers: Dict[Extractor, List[yara.Match]]) -> List[dict]:
38
+ results = list()
39
+ for parser, yara_matches in parsers.items():
40
+ # Just run CAPE parsers as-is
41
+ try:
42
+ result = self.result_template(parser, yara_matches)
43
+ if parser.venv:
44
+ raise NotImplementedError()
45
+ else:
46
+ cfg = parser.module.extract_config(open(sample_path, "rb").read())
47
+
48
+ if not (cfg or yara_matches):
49
+ # Nothing of interest to report
50
+ continue
51
+
52
+ if cfg:
53
+ result.update({"config": ExtractorModel(**cfg).dict(exclude_defaults=True, exclude_none=True)})
54
+ except Exception as e:
55
+ # If an exception was raised at runtime, append to results
56
+ result["exception"] = str(e)
57
+ self.log.error(f"{parser.id}: {e}")
58
+
59
+ results.append(result)
60
+
61
+ return results
@@ -36,42 +36,47 @@ with open("{output_path}", 'w') as fp:
36
36
 
37
37
  def validate(self, module: Any) -> bool:
38
38
  if inspect.isclass(module):
39
- return issubclass(module, MACO_Extractor)
39
+ # 'author' has to be implemented otherwise will raise an exception according to MWCP
40
+ return issubclass(module, MACO_Extractor) and module.author
40
41
 
41
- def run(self, sample_path: str, parsers: Dict[Extractor, List[str]]) -> Dict[str, dict]:
42
- results = dict()
42
+ def result_template(self, extractor: Extractor, yara_matches: List) -> Dict[str, str]:
43
+ template = super().result_template(extractor, yara_matches)
44
+ decoder = extractor.module()
45
+ template.update(
46
+ {
47
+ "author": decoder.author,
48
+ "description": decoder.__doc__,
49
+ "config": {"family": decoder.family if hasattr(decoder, "family") else decoder.__class__.__name__},
50
+ }
51
+ )
52
+ return template
53
+
54
+ def run(self, sample_path: str, parsers: Dict[Extractor, List[str]]) -> List[dict]:
55
+ results = list()
43
56
  for extractor, yara_matches in parsers.items():
44
57
  try:
45
- decoder: Extractor = extractor.module()
46
- config = {}
47
- if hasattr(decoder, "family") and decoder.family:
48
- config["family"] = decoder.family
49
- else:
50
- config["family"] = decoder.__class__.__name__
58
+ decoder = extractor.module()
59
+ result = self.result_template(extractor, yara_matches)
60
+
51
61
  # Run MaCo parser with YARA matches
52
- results[decoder.name] = {
53
- "author": decoder.author,
54
- "description": decoder.__doc__,
55
- "config": config,
56
- }
57
- result: ExtractorModel = None
62
+ r: ExtractorModel = None
58
63
  if extractor.venv:
59
64
  # Run in special mode using the virtual environment detected
60
- result = self.run_in_venv(sample_path, extractor)
65
+ r = self.run_in_venv(sample_path, extractor)
61
66
  else:
62
- result = decoder.run(open(sample_path, "rb"), matches=yara_matches)
63
- if result:
64
- results[decoder.name].update({"config": result.dict(exclude_defaults=True, exclude_none=True)})
65
- elif yara_matches:
66
- # YARA rules matched, but no configuration extracted
67
+ r = decoder.run(open(sample_path, "rb"), matches=yara_matches)
68
+
69
+ if not (r or yara_matches):
70
+ # Nothing to report
67
71
  continue
68
- else:
69
- # No result
70
- results.pop(decoder.name, None)
72
+ if r:
73
+ result.update({"config": r.dict(exclude_defaults=True, exclude_none=True)})
74
+
71
75
  except Exception as e:
72
76
  # Add exception to results
73
- results[decoder.name]["exception"] = str(e)
74
- self.log.error(e)
77
+ result["exception"] = str(e)
78
+ self.log.error(f"{extractor.id}: {e}")
79
+ results.append(result)
75
80
  return results
76
81
 
77
82
  def run_in_venv(self, sample_path: str, extractor: Extractor) -> ExtractorModel:
@@ -2,13 +2,14 @@
2
2
 
3
3
  import inspect
4
4
  from logging import Logger
5
+ from typing import Dict, List
5
6
 
6
7
  import mwcp
7
8
  import regex
8
9
  from maco.model import ConnUsageEnum, Encryption, ExtractorModel
9
10
  from mwcp import Parser
10
11
 
11
- from configextractor.frameworks.base import Framework
12
+ from configextractor.frameworks.base import Extractor, Framework
12
13
 
13
14
  IP_REGEX_ONLY = r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
14
15
 
@@ -197,53 +198,56 @@ with open("{output_path}", 'w') as fp:
197
198
 
198
199
  def validate(self, parser):
199
200
  if inspect.isclass(parser):
200
- return issubclass(parser, Parser) and (parser.AUTHOR or parser.DESCRIPTION)
201
+ # 'DESCRIPTION' has to be implemented otherwise will raise an exception according to MWCP
202
+ return issubclass(parser, Parser) and parser.DESCRIPTION
203
+
204
+ def result_template(self, extractor: Extractor, yara_matches: List) -> Dict[str, str]:
205
+ template = super().result_template(extractor, yara_matches)
206
+ template.update(
207
+ {
208
+ "author": extractor.module.AUTHOR,
209
+ "description": extractor.module.DESCRIPTION,
210
+ }
211
+ )
212
+ return template
201
213
 
202
214
  def run(self, sample_path, parsers):
203
- results = dict()
215
+ results = list()
204
216
 
205
217
  for parser, yara_matches in parsers.items():
206
218
  parser_name = MWCP.get_name(parser)
207
219
  try:
208
- results[parser_name] = {
209
- "author": parser.module.AUTHOR,
210
- "description": parser.module.DESCRIPTION,
211
- "config": {},
212
- }
220
+ result = self.result_template(parser, yara_matches)
213
221
 
214
- result: dict = None
222
+ r: dict = None
215
223
  if parser.venv:
216
- result = self.run_in_venv(sample_path, parser)
224
+ r = self.run_in_venv(sample_path, parser)
217
225
  else:
218
226
  # Just run MWCP parsers directly, using the filename to fetch the class attribute from module
219
- result = mwcp.run(parser.module, data=open(sample_path, "rb").read()).as_json_dict()
227
+ r = mwcp.run(parser.module, data=open(sample_path, "rb").read()).as_json_dict()
220
228
 
221
229
  # Log any errors raised during execution
222
- [self.log.error(e) for e in result["errors"]]
223
- result = convert_to_MACO(result["metadata"])
224
- if result or yara_matches:
225
- family = parser_name
226
- for y in yara_matches:
227
- if y.meta.get("malware"):
228
- family = y.meta["malware"]
229
- break
230
-
231
- result["family"] = family
232
- results[parser_name].update(
233
- {
234
- "config": ExtractorModel(**result).dict(exclude_defaults=True, exclude_none=True),
235
- }
236
- )
237
-
238
- if not results[parser_name]["config"]:
239
- results.pop(parser_name, None)
240
- elif yara_matches:
241
- # YARA rules matched, but no configuration extracted
230
+ [self.log.error(e) for e in r["errors"]]
231
+ r = convert_to_MACO(r["metadata"])
232
+ if not (r or yara_matches):
233
+ # Nothing of interest to report
242
234
  continue
243
- else:
244
- # No result
245
- results.pop(parser_name, None)
235
+
236
+ family = parser_name
237
+ for y in yara_matches:
238
+ if y.meta.get("malware"):
239
+ family = y.meta["malware"]
240
+ break
241
+
242
+ r["family"] = family
243
+ result.update(
244
+ {
245
+ "config": ExtractorModel(**r).dict(exclude_defaults=True, exclude_none=True),
246
+ }
247
+ )
248
+
246
249
  except Exception as e:
247
- results[parser_name]["exception"] = str(e)
248
- self.log.error(f"{parser_name}: {e}")
250
+ result["exception"] = str(e)
251
+ self.log.error(f"{parser.id}: {e}")
252
+ results.append(result)
249
253
  return results
@@ -13,6 +13,7 @@ from typing import Dict, List, Set
13
13
  import cart
14
14
  import regex
15
15
  import yara
16
+
16
17
  from configextractor.frameworks import CAPE, MACO, MWCP
17
18
  from configextractor.frameworks.base import Extractor, Framework
18
19
 
@@ -34,9 +35,9 @@ class ConfigExtractor:
34
35
  }
35
36
 
36
37
  self.parsers: Dict[str, Extractor] = dict()
37
- yara_rules: List[str] = list()
38
- yara_rule_names: List[str] = list()
38
+ namespaced_yara_rules: Dict[str, List[str]] = dict()
39
39
  for parsers_dir in parsers_dirs:
40
+ parsers_dir = os.path.abspath(parsers_dir)
40
41
  self.log.debug("Adding directories within parser directory in case of local dependencies")
41
42
  self.log.debug(f"Adding {os.path.join(parsers_dir, os.pardir)} to PATH")
42
43
  not_py = [
@@ -132,8 +133,9 @@ class ConfigExtractor:
132
133
  if member.__name__ != module_name:
133
134
  # Account for the possibility of multiple extractor classes within the same module
134
135
  module_id = f"{module_name}.{member.__name__}"
135
- rules = fw_class.extract_yara_from_module(member, module_id, yara_rule_names) or []
136
+ rules = fw_class.extract_yara_from_module(member)
136
137
  ext = Extractor(
138
+ module_id,
137
139
  fw_name,
138
140
  member,
139
141
  module.__file__,
@@ -142,7 +144,7 @@ class ConfigExtractor:
142
144
  parser_venv,
143
145
  )
144
146
  if rules:
145
- yara_rules.extend(rules)
147
+ namespaced_yara_rules[module_id] = rules
146
148
  self.parsers[module_id] = ext
147
149
  break
148
150
  except TypeError:
@@ -150,10 +152,6 @@ class ConfigExtractor:
150
152
  except Exception as e:
151
153
  self.log.error(f"{member}: {e}")
152
154
 
153
- # Correct metadata in YARA rules
154
- if original_dir != parsers_dir:
155
- yara_rules = [rule.replace(parsers_dir, original_dir) for rule in yara_rules]
156
-
157
155
  if original_dir != parsers_dir:
158
156
  # Correct the paths to the parsers to match metadata changes
159
157
  sys.path.remove(path_parent)
@@ -166,7 +164,7 @@ class ConfigExtractor:
166
164
  parser_obj.root_directory = original_dir
167
165
  shutil.rmtree(parsers_dir)
168
166
 
169
- self.yara = yara.compile(source="\n".join(yara_rules))
167
+ self.yara = yara.compile(sources={ns: "\n".join(rules) for ns, rules in namespaced_yara_rules.items()})
170
168
  for fw_name in list(self.FRAMEWORK_LIBRARY_MAPPING.keys()):
171
169
  self.log.debug(
172
170
  f"# of YARA-dependent parsers under {fw_name}: "
@@ -176,7 +174,6 @@ class ConfigExtractor:
176
174
  f"# of YARA-independent parsers under {fw_name}: "
177
175
  f"{len([p for p in self.parsers.values() if not p.rule and p.framework == fw_name])}"
178
176
  )
179
- self.log.debug(f"# of YARA rules extracted from all parsers: {len(yara_rules)}")
180
177
  if parser_blocklist:
181
178
  self.log.info(f"Ignoring output from the following parsers matching: {parser_blocklist}")
182
179
 
@@ -192,10 +189,12 @@ class ConfigExtractor:
192
189
 
193
190
  def finalize(self, results: List[dict]):
194
191
  # Ensure schemes/protocol are present in HTTP configurations
195
- for config in results.values():
192
+ for config in results:
196
193
  config = config.get("config", {})
197
194
  for network_conn in config.get("http", []):
198
195
  network_conn.setdefault("protocol", "http")
196
+ # Ensure protocol is lowercased
197
+ network_conn["protocol"] = network_conn["protocol"].lower()
199
198
  uri: str = network_conn.get("uri")
200
199
  if uri and not uri.startswith(network_conn["protocol"]):
201
200
  # Ensure URI starts with protocol
@@ -222,7 +221,7 @@ class ConfigExtractor:
222
221
  # Get YARA-dependent parsers that should run based on match
223
222
  for yara_match in self.yara.match(sample_copy.name):
224
223
  # Retrieve relevant parser information
225
- extractor = self.parsers[yara_match.meta.get("parser_module")]
224
+ extractor = self.parsers[yara_match.namespace]
226
225
  if block_regex and block_regex.match(extractor.module.__name__):
227
226
  self.log.info(f"Blocking {extractor.module.__name__} based on passed blocklist regex list")
228
227
  continue
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: configextractor-py
3
- Version: 1.0.dev0
3
+ Version: 1.1.0
4
4
  Summary: A library for extracting malware configurations across multiple frameworks
5
5
  Description-Content-Type: text/markdown
6
6
  Requires-Dist: cart
@@ -20,4 +20,11 @@ configextractor_py.egg-info/dependency_links.txt
20
20
  configextractor_py.egg-info/entry_points.txt
21
21
  configextractor_py.egg-info/requires.txt
22
22
  configextractor_py.egg-info/top_level.txt
23
- pipelines/publish.yaml
23
+ pipelines/publish.yaml
24
+ pipelines/test.yaml
25
+ tests/requirements.txt
26
+ tests/test_detection.py
27
+ tests/test_run.py
28
+ tests/test_parsers/cape.py
29
+ tests/test_parsers/maco.py
30
+ tests/test_parsers/mwcp.py
@@ -30,16 +30,6 @@ jobs:
30
30
  ls dist
31
31
  displayName: Build
32
32
 
33
- - script: |
34
- set -xv # Echo commands before they are run
35
- sudo env "PATH=$PATH" python -m pip install --no-cache-dir twine
36
- ls dist
37
- twine upload --skip-existing --repository-url $TEST_REPOSITORY_URL dist/*
38
- displayName: Deploy to Test PyPI
39
- env:
40
- TWINE_USERNAME: $(twineUsername)
41
- TWINE_PASSWORD: $(twinePassword)
42
-
43
33
  - script: |
44
34
  set -xv # Echo commands before they are run
45
35
  sudo env "PATH=$PATH" python -m pip install --no-cache-dir twine
@@ -0,0 +1,40 @@
1
+ name: tests
2
+
3
+ trigger: ["*"]
4
+ pr: ["*"]
5
+
6
+ pool:
7
+ vmImage: "ubuntu-20.04"
8
+
9
+ jobs:
10
+ - job: run_test
11
+ strategy:
12
+ matrix:
13
+ Python3_9:
14
+ python.version: "3.9"
15
+ Python3_10:
16
+ python.version: "3.10"
17
+ Python3_11:
18
+ python.version: "3.11"
19
+ # TODO: Look at cart support
20
+ # Python3_12:
21
+ # python.version: "3.12"
22
+
23
+ timeoutInMinutes: 10
24
+
25
+ steps:
26
+ - task: UsePythonVersion@0
27
+ displayName: Set python version
28
+ inputs:
29
+ versionSpec: "$(python.version)"
30
+ - script: |
31
+ [ ! -d "$(pwd)/tests" ] && echo "No tests found" && exit
32
+ [ -f $(pwd)/requirements.txt ] && sudo env "PATH=$PATH" python -m pip install -U --no-cache-dir -r $(pwd)/requirements.txt
33
+ [ -f $(pwd)/tests/requirements.txt ] && sudo env "PATH=$PATH" python -m pip install -U --no-cache-dir -r $(pwd)/tests/requirements.txt
34
+ sudo rm -rf /tmp/* /var/lib/apt/lists/* ~/.cache/pip
35
+ displayName: Setup environment
36
+ - script: |
37
+ [ ! -d "$(pwd)/tests" ] && echo "No tests found" && exit
38
+ export REPO_NAME=${BUILD_REPOSITORY_NAME##*/}
39
+ python -m pytest -p no:cacheprovider --durations=10 -rsx -vv
40
+ displayName: Test
@@ -0,0 +1 @@
1
+ pytest
@@ -0,0 +1,32 @@
1
+ import pytest
2
+
3
+ from configextractor.main import ConfigExtractor
4
+
5
+
6
+ @pytest.fixture
7
+ def cx():
8
+ yield ConfigExtractor(["tests/test_parsers"])
9
+
10
+
11
+ def test_general_detection(cx):
12
+ # Check to see if we actually detected any of the test parsers
13
+ assert cx.parsers
14
+
15
+
16
+ def test_cape_detection(cx):
17
+ # Ensure the CAPE parser was detected and NOT the class wrapping a similar CAPE function signature
18
+ # A confusion in detection can throw off automated systems like Assemblyline
19
+ assert "test_parsers.cape" in cx.parsers
20
+ assert "test_parsers.cape.CAPEWrapper" not in cx.parsers
21
+
22
+
23
+ def test_maco_detection(cx):
24
+ # Ensure the subclass was detected
25
+ assert "test_parsers.maco.TestMACO" in cx.parsers
26
+ assert "test_parsers.maco.Extractor" not in cx.parsers
27
+
28
+
29
+ def test_mwcp_detection(cx):
30
+ # Ensure the subclass was detected
31
+ assert "test_parsers.mwcp.TestMWCP" in cx.parsers
32
+ assert "test_parsers.mwcp.Parser" not in cx.parsers
@@ -0,0 +1,11 @@
1
+ # Format of CAPE parsers that are detected by library
2
+ def extract_config(data):
3
+ ...
4
+
5
+
6
+ # Similar function signatures to CAPE's included in another class
7
+ # This shouldn't be detected under the CAPE framework
8
+ # (ie. if found under a MACO implementation, then the detected framework will be MACO)
9
+ class CAPEWrapper:
10
+ def extract_config(self):
11
+ ...
@@ -0,0 +1,6 @@
1
+ from maco.extractor import Extractor
2
+
3
+
4
+ # Format of MACO parsers that are detected by library
5
+ class TestMACO(Extractor):
6
+ author = "cccs-rs"
@@ -0,0 +1,6 @@
1
+ from mwcp import Parser
2
+
3
+
4
+ # Format of MWCP parsers that are detected by library
5
+ class TestMWCP(Parser):
6
+ DESCRIPTION = "test"
@@ -0,0 +1,15 @@
1
+ import pytest
2
+
3
+ from configextractor.main import ConfigExtractor
4
+
5
+
6
+ @pytest.fixture
7
+ def cx():
8
+ yield ConfigExtractor(["tests/test_parsers"])
9
+
10
+
11
+ def test_finalize_uri(cx):
12
+ # Bug: URIs prepended with an extra protocol because it thought it was missing because of casing difference
13
+ results = [{"config": {"http": [{"uri": "https://bad.com", "protocol": "HTTPS"}]}}]
14
+ cx.finalize(results)
15
+ assert results[0]["config"]["http"][0]["uri"] == "https://bad.com"
@@ -1,53 +0,0 @@
1
- # CAPE framework
2
- from collections import defaultdict
3
- from inspect import signature
4
-
5
- from maco.model import ExtractorModel
6
-
7
- from configextractor.frameworks.base import Extractor, Framework
8
-
9
-
10
- class CAPE(Framework):
11
- @staticmethod
12
- def get_classification(extractor: Extractor):
13
- if hasattr(extractor.module, "TLP"):
14
- return extractor.module.TLP
15
- return None
16
-
17
- def validate(self, module):
18
- return hasattr(module, "extract_config") and len(signature(module.extract_config).parameters) == 1
19
-
20
- def run(self, sample_path, parsers):
21
- results = defaultdict(dict)
22
- for parser, yara_matches in parsers.items():
23
- # Just run CAPE parsers as-is
24
- parser_name = CAPE.get_name(parser)
25
- try:
26
- results[parser_name].update(
27
- {
28
- "author": parser.module.AUTHOR if hasattr(parser.module, "AUTHOR") else "<MISSING_AUTHOR>",
29
- "description": parser.module.DESCRIPTION
30
- if hasattr(parser.module, "DESCRIPTION")
31
- else "<MISSING_DESCRIPTION>",
32
- "config": {},
33
- }
34
- )
35
- if parser.venv:
36
- raise NotImplementedError()
37
- else:
38
- result = parser.module.extract_config(open(sample_path, "rb").read())
39
- if result:
40
- results[parser_name].update(
41
- {"config": ExtractorModel(**result).dict(exclude_defaults=True, exclude_none=True)}
42
- )
43
- elif yara_matches:
44
- # YARA rules matched, but no configuration extracted
45
- continue
46
- else:
47
- # No result
48
- results.pop(parser_name, None)
49
- except Exception as e:
50
- results[parser_name]["exception"] = str(e)
51
- self.log.error(f"{parser_name}: {e}")
52
-
53
- return results