Skip to content

Commit 0b70aba

Browse files
committed
improve base file class
Signed-off-by: pranjalg1331 <[email protected]>
1 parent 38ef09b commit 0b70aba

14 files changed

+675
-33
lines changed

tests/api_app/analyzers_manager/unit_tests/file_analyzers/base_test_class.py

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import hashlib
2+
import logging
23
import os
34
from contextlib import ExitStack
45
from types import SimpleNamespace
@@ -7,6 +8,8 @@
78
from api_app.analyzers_manager.models import AnalyzerConfig
89
from tests.mock_utils import MockUpResponse
910

11+
logger = logging.getLogger(__name__)
12+
1013

1114
class BaseFileAnalyzerTest(TestCase):
1215
analyzer_class = None
@@ -59,71 +62,83 @@ def get_all_supported_mimetypes(cls) -> set:
5962
return set(cls.MIMETYPE_TO_FILENAME.keys())
6063

6164
@classmethod
62-
def get_extra_config(self) -> dict:
65+
def get_extra_config(cls) -> dict:
6366
"""
6467
Subclasses can override this to provide additional runtime configuration
6568
specific to their analyzer (e.g., API keys, URLs, retry counts, etc.).
66-
67-
Returns:
68-
dict: Extra configuration parameters for the analyzer
6969
"""
7070
return {}
7171

7272
def get_mocked_response(self):
7373
"""
7474
Subclasses override this to define expected mocked output.
75-
76-
Can return:
77-
1. A single patch object: patch('module.function')
78-
2. A list of patch objects: [patch('module.func1'), patch('module.func2')]
79-
3. A context manager: patch.multiple() or ExitStack()
8075
"""
81-
raise NotImplementedError
76+
raise NotImplementedError("Subclasses must implement get_mocked_response()")
8277

8378
@classmethod
84-
def _apply_patches(self, patches):
79+
def _apply_patches(cls, patches):
8580
"""Helper method to apply single or multiple patches"""
8681
if patches is None:
8782
return ExitStack() # No-op context manager
8883

89-
# If it's already a context manager, return as-is
9084
if hasattr(patches, "__enter__") and hasattr(patches, "__exit__"):
9185
return patches
9286

93-
# If it's a list of patches, use ExitStack to manage them
9487
if isinstance(patches, (list, tuple)):
9588
stack = ExitStack()
9689
for patch_obj in patches:
9790
stack.enter_context(patch_obj)
9891
return stack
9992

100-
# Single patch object
10193
return patches
10294

95+
def setUp(self):
96+
super().setUp()
97+
logger.info("Setting up test environment for file analyzer")
98+
if self.analyzer_class:
99+
analyzer_module = self.analyzer_class.__module__
100+
logging.getLogger(analyzer_module).setLevel(logging.CRITICAL)
101+
logging.getLogger("api_app.analyzers_manager").setLevel(logging.WARNING)
102+
103+
def tearDown(self):
104+
super().tearDown()
105+
logger.info("Tearing down test environment for file analyzer")
106+
if self.analyzer_class:
107+
analyzer_module = self.analyzer_class.__module__
108+
logging.getLogger(analyzer_module).setLevel(logging.NOTSET)
109+
logging.getLogger("api_app.analyzers_manager").setLevel(logging.NOTSET)
110+
103111
def test_analyzer_on_supported_filetypes(self):
104112
if self.analyzer_class is None:
105113
self.skipTest("analyzer_class is not set")
106-
config = AnalyzerConfig.objects.get(
107-
python_module=self.analyzer_class.python_module
108-
)
109-
print(config)
110114

111-
# If supported_filetypes is None or empty, use all available mimetypes
112-
if config.supported_filetypes:
113-
supported_types = config.supported_filetypes
114-
else:
115-
supported_types = self.get_all_supported_mimetypes()
115+
logger.info("Starting file analyzer test for: %s", self.analyzer_class.__name__)
116+
117+
try:
118+
config = AnalyzerConfig.objects.get(
119+
python_module=self.analyzer_class.python_module
120+
)
121+
except AnalyzerConfig.DoesNotExist:
122+
self.fail(
123+
f"No AnalyzerConfig found for {self.analyzer_class.python_module}"
124+
)
125+
126+
logger.debug("Loaded analyzer config: %s", config)
127+
128+
supported_types = (
129+
config.supported_filetypes or self.get_all_supported_mimetypes()
130+
)
116131

117132
for mimetype in supported_types:
118133
with self.subTest(mimetype=mimetype):
134+
logger.info("Testing mimetype: %s", mimetype)
119135

120136
try:
121137
file_bytes = self.get_sample_file_bytes(mimetype)
122-
except (ValueError, OSError):
123-
print(f"SKIPPING {mimetype}")
138+
except (ValueError, OSError) as e:
139+
logger.warning("Skipping %s due to error: %s", mimetype, str(e))
124140
continue
125141

126-
# Apply patches using the improved system
127142
patches = self.get_mocked_response()
128143
with self._apply_patches(patches):
129144
md5 = hashlib.md5(file_bytes).hexdigest()
@@ -133,26 +148,37 @@ def test_analyzer_on_supported_filetypes(self):
133148
analyzer.filename = f"test_file_{mimetype}"
134149
analyzer.md5 = md5
135150
analyzer.read_file_bytes = lambda: file_bytes
151+
136152
analyzer._job = SimpleNamespace()
137153
analyzer._job.analyzable = SimpleNamespace()
138154
analyzer._job.analyzable.name = analyzer.filename
139155
analyzer._job.analyzable.mimetype = mimetype
140156
analyzer._job.analyzable.sha256 = hashlib.sha256(
141157
file_bytes
142158
).hexdigest()
159+
analyzer._job_id = ""
143160

144-
test_file_path = self.get_sample_file_path(mimetype)
145-
analyzer._FileAnalyzer__filepath = test_file_path
161+
analyzer._FileAnalyzer__filepath = self.get_sample_file_path(
162+
mimetype
163+
)
146164

147-
extra_config = self.get_extra_config()
148-
for key, value in extra_config.items():
165+
for key, value in self.get_extra_config().items():
149166
setattr(analyzer, key, value)
150167

151-
response = analyzer.run()
168+
try:
169+
response = analyzer.run()
170+
logger.info("Analyzer ran successfully for %s", mimetype)
171+
except Exception as e:
172+
logger.exception(
173+
"Analyzer raised an exception for %s", mimetype
174+
)
175+
self.fail(
176+
f"Analyzer run failed for {mimetype}: {type(e).__name__}: {e}"
177+
)
178+
152179
self.assertIsInstance(
153180
response,
154181
(dict, MockUpResponse),
155182
f"Expected dict or MockUpResponse, got {type(response)} with value: {response}",
156183
)
157-
158-
print(f"SUCCESS {mimetype}")
184+
logger.debug("Successful result for %s: %s", mimetype, response)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
from api_app.analyzers_manager.file_analyzers.quark_engine import QuarkEngine
4+
5+
from .base_test_class import BaseFileAnalyzerTest
6+
7+
8+
class TestQuarkEngine(BaseFileAnalyzerTest):
9+
analyzer_class = QuarkEngine
10+
11+
def get_extra_config(self):
12+
return {}
13+
14+
def get_mocked_response(self):
15+
# Mock the Report class and its methods
16+
mock_report = MagicMock()
17+
mock_json_report = {
18+
"crimes": [
19+
{
20+
"crime": "Send SMS",
21+
"weight": 50,
22+
"confidence": "80%",
23+
"permissions": ["android.permission.SEND_SMS"],
24+
"api": [
25+
{
26+
"class": "Landroid/telephony/SmsManager",
27+
"method": "sendTextMessage",
28+
}
29+
],
30+
"register": [
31+
{"class": "Lcom/example/MainActivity", "method": "onCreate"}
32+
],
33+
}
34+
],
35+
"total_score": 50,
36+
"summary": {"threat_level": "Medium", "total_crimes": 1},
37+
}
38+
mock_report.get_report.return_value = mock_json_report
39+
40+
return [
41+
# Mock the Report class from where it's actually imported
42+
patch("quark.report.Report", return_value=mock_report),
43+
# Mock the DIR_PATH from where it's actually imported
44+
patch("quark.config.DIR_PATH", "/mock/rules/path"),
45+
]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# RTFInfo Test Class
2+
from unittest.mock import MagicMock, patch
3+
4+
from api_app.analyzers_manager.file_analyzers.rtf_info import RTFInfo
5+
6+
from .base_test_class import BaseFileAnalyzerTest
7+
8+
9+
class TestRTFInfo(BaseFileAnalyzerTest):
10+
analyzer_class = RTFInfo
11+
12+
def get_extra_config(self):
13+
return {}
14+
15+
def get_mocked_response(self):
16+
# Create mock RTF objects
17+
mock_rtf_obj1 = MagicMock()
18+
mock_rtf_obj1.is_ole = True
19+
mock_rtf_obj1.class_name = b"OLE2Link"
20+
mock_rtf_obj1.format_id = 2
21+
mock_rtf_obj1.oledata_size = 1024
22+
mock_rtf_obj1.is_package = False
23+
mock_rtf_obj1.oledata_md5 = "abc123def456"
24+
mock_rtf_obj1.clsid = "00000000-0000-0000-C000-000000000046"
25+
mock_rtf_obj1.clsid_desc = "OLE Link Object"
26+
27+
mock_rtf_obj2 = MagicMock()
28+
mock_rtf_obj2.is_ole = True
29+
mock_rtf_obj2.class_name = b"Equation.3"
30+
mock_rtf_obj2.format_id = 1
31+
mock_rtf_obj2.oledata_size = 2048
32+
mock_rtf_obj2.is_package = True
33+
mock_rtf_obj2.filename = "malicious.exe"
34+
mock_rtf_obj2.src_path = "C:\\temp\\malicious.exe"
35+
mock_rtf_obj2.temp_path = "C:\\temp\\temp123.tmp"
36+
mock_rtf_obj2.olepkgdata_md5 = "def456ghi789"
37+
mock_rtf_obj2.clsid = None
38+
39+
# Create mock parser instance
40+
mock_parser_instance = MagicMock()
41+
mock_parser_instance.objects = [mock_rtf_obj1, mock_rtf_obj2]
42+
mock_parser_instance.parse.return_value = None
43+
44+
return [
45+
patch(
46+
"api_app.analyzers_manager.file_analyzers.rtf_info.RtfObjParser",
47+
return_value=mock_parser_instance,
48+
),
49+
]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
from api_app.analyzers_manager.file_analyzers.signature_info import SignatureInfo
4+
5+
from .base_test_class import BaseFileAnalyzerTest
6+
7+
8+
class TestSignatureInfo(BaseFileAnalyzerTest):
9+
analyzer_class = SignatureInfo
10+
11+
def get_extra_config(self):
12+
return {}
13+
14+
def get_mocked_response(self):
15+
# Create mock process
16+
mock_process = MagicMock()
17+
mock_process.returncode = 0
18+
mock_process.communicate.return_value = (
19+
b"Signature verification: ok\nCertificate info:\n Subject: CN=Test Certificate\n Issuer: CN=Test CA\n",
20+
b"",
21+
)
22+
23+
return [
24+
patch(
25+
"api_app.analyzers_manager.file_analyzers.signature_info.Popen",
26+
return_value=mock_process,
27+
),
28+
patch(
29+
"api_app.analyzers_manager.file_analyzers.signature_info.settings.PROJECT_LOCATION",
30+
"/mock/project",
31+
),
32+
]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from unittest.mock import patch
2+
3+
from api_app.analyzers_manager.file_analyzers.strings_info import StringsInfo
4+
5+
from .base_test_class import BaseFileAnalyzerTest
6+
7+
8+
class TestStringsInfo(BaseFileAnalyzerTest):
9+
analyzer_class = StringsInfo
10+
11+
def get_extra_config(self):
12+
return {
13+
"max_number_of_strings": 100,
14+
"max_characters_for_string": 200,
15+
"rank_strings": 1, # Enable string ranking
16+
}
17+
18+
def get_mocked_response(self):
19+
# Mock strings extracted from binary
20+
mock_strings_first_call = [
21+
"http://malicious-site.com/payload",
22+
"CreateFileA",
23+
"WriteFile",
24+
"RegSetValueA",
25+
"https://c2server.evil/beacon",
26+
"malware.exe",
27+
"C:\\Windows\\System32\\cmd.exe",
28+
"ftp://badsite.com/data",
29+
"Some random string",
30+
"Another test string",
31+
]
32+
33+
# Mock ranked strings (second call when rank_strings is enabled)
34+
mock_ranked_strings = [
35+
"http://malicious-site.com/payload",
36+
"https://c2server.evil/beacon",
37+
"CreateFileA",
38+
"ftp://badsite.com/data",
39+
"WriteFile",
40+
"RegSetValueA",
41+
"malware.exe",
42+
"C:\\Windows\\System32\\cmd.exe",
43+
]
44+
45+
return [
46+
patch(
47+
"api_app.analyzers_manager.file_analyzers.strings_info.StringsInfo._docker_run",
48+
side_effect=[
49+
mock_strings_first_call, # First call for flarestrings
50+
mock_ranked_strings, # Second call for rank_strings (if enabled)
51+
],
52+
),
53+
]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from unittest.mock import patch
2+
3+
from api_app.analyzers_manager.file_analyzers.suricata import Suricata
4+
5+
from .base_test_class import BaseFileAnalyzerTest
6+
7+
8+
class TestSuricata(BaseFileAnalyzerTest):
9+
analyzer_class = Suricata
10+
11+
def get_extra_config(self):
12+
return {"reload_rules": True, "extended_logs": False, "signatures": {}}
13+
14+
def get_mocked_response(self):
15+
# Mock Suricata analysis results with network alerts
16+
mock_suricata_report = {
17+
"data": [],
18+
"stats": {
19+
"capture": {"kernel_packets": 150, "kernel_drops": 0},
20+
"decoder": {
21+
"pkts": 150,
22+
"bytes": 45600,
23+
"invalid": 0,
24+
"ipv4": 148,
25+
"ipv6": 2,
26+
"ethernet": 150,
27+
"tcp": 100,
28+
"udp": 48,
29+
"icmpv4": 2,
30+
},
31+
},
32+
}
33+
34+
return [
35+
patch(
36+
"api_app.analyzers_manager.file_analyzers.suricata.Suricata._docker_run",
37+
return_value=mock_suricata_report,
38+
),
39+
]

0 commit comments

Comments
 (0)