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.
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/.vscode/settings.json +4 -0
- {configextractor-py-1.0.dev0/configextractor_py.egg-info → configextractor-py-1.1.0}/PKG-INFO +1 -1
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/base.py +13 -31
- configextractor-py-1.1.0/configextractor/frameworks/cape.py +61 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/maco.py +31 -26
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/mwcp.py +40 -36
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/main.py +11 -12
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0/configextractor_py.egg-info}/PKG-INFO +1 -1
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/SOURCES.txt +8 -1
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/pipelines/publish.yaml +0 -10
- configextractor-py-1.1.0/pipelines/test.yaml +40 -0
- configextractor-py-1.1.0/tests/requirements.txt +1 -0
- configextractor-py-1.1.0/tests/test_detection.py +32 -0
- configextractor-py-1.1.0/tests/test_parsers/cape.py +11 -0
- configextractor-py-1.1.0/tests/test_parsers/maco.py +6 -0
- configextractor-py-1.1.0/tests/test_parsers/mwcp.py +6 -0
- configextractor-py-1.1.0/tests/test_run.py +15 -0
- configextractor-py-1.0.dev0/configextractor/frameworks/cape.py +0 -53
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/.gitignore +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/.gitmodules +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/README.md +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/__init__.py +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/cli.py +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/__init__.py +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/malduck.py +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/ratdecoder.py.bak +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/dependency_links.txt +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/entry_points.txt +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/requires.txt +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/top_level.txt +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/requirements.txt +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/setup.cfg +0 -0
- {configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/setup.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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]]) ->
|
|
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
|
-
|
|
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
|
|
42
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
r = self.run_in_venv(sample_path, extractor)
|
|
61
66
|
else:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
209
|
-
"author": parser.module.AUTHOR,
|
|
210
|
-
"description": parser.module.DESCRIPTION,
|
|
211
|
-
"config": {},
|
|
212
|
-
}
|
|
220
|
+
result = self.result_template(parser, yara_matches)
|
|
213
221
|
|
|
214
|
-
|
|
222
|
+
r: dict = None
|
|
215
223
|
if parser.venv:
|
|
216
|
-
|
|
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
|
-
|
|
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
|
|
223
|
-
|
|
224
|
-
if
|
|
225
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
self.log.error(f"{
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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
|
{configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/SOURCES.txt
RENAMED
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/__init__.py
RENAMED
|
File without changes
|
{configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor/frameworks/malduck.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/requires.txt
RENAMED
|
File without changes
|
{configextractor-py-1.0.dev0 → configextractor-py-1.1.0}/configextractor_py.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|