Skip to content

Commit 416b018

Browse files
committed
test(connectors): add unit tests for MISP bulk add_event
1 parent 327e86f commit 416b018

1 file changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl
2+
# See the file 'LICENSE' for copying permission.
3+
4+
from unittest.mock import MagicMock, patch
5+
6+
from kombu import uuid
7+
8+
from api_app.analyzables_manager.models import Analyzable
9+
from api_app.choices import Classification
10+
from api_app.connectors_manager.connectors.misp import MISP
11+
from api_app.connectors_manager.models import ConnectorConfig, ConnectorReport
12+
from api_app.models import Job, Parameter, PluginConfig
13+
from tests import CustomTestCase
14+
15+
16+
class MISPConnectorTestCase(CustomTestCase):
17+
fixtures = [
18+
"api_app/fixtures/0001_user.json",
19+
]
20+
21+
# -- Helpers --
22+
23+
@staticmethod
24+
def _get_misp_config():
25+
return ConnectorConfig.objects.get(name="MISP")
26+
27+
@staticmethod
28+
def _create_plugin_configs(config):
29+
pcs = []
30+
for name in ("url_key_name", "api_key_name"):
31+
param = Parameter.objects.get(python_module=config.python_module, name=name)
32+
pc = PluginConfig.objects.create(
33+
parameter=param,
34+
value="https://misp.test" if "url" in name else "test-api-key",
35+
for_organization=False,
36+
owner=None,
37+
connector_config=config,
38+
)
39+
pcs.append(pc)
40+
return pcs
41+
42+
def _setup_job(self):
43+
config = self._get_misp_config()
44+
pcs = self._create_plugin_configs(config)
45+
analyzable = Analyzable.objects.create(name="8.8.8.8", classification=Classification.IP)
46+
job = Job.objects.create(
47+
analyzable=analyzable,
48+
user=self.superuser,
49+
status=Job.STATUSES.REPORTED_WITHOUT_FAILS.value,
50+
)
51+
job.connectors_to_execute.set([config])
52+
return job, config, pcs
53+
54+
@staticmethod
55+
def _cleanup(job, config, pcs):
56+
try:
57+
ConnectorReport.objects.get(job=job, config=config).delete()
58+
except ConnectorReport.DoesNotExist:
59+
pass
60+
analyzable = job.analyzable
61+
job.delete()
62+
analyzable.delete()
63+
for pc in pcs:
64+
pc.delete()
65+
66+
# -- Tests --
67+
68+
@patch.object(MISP, "_monkeypatch", classmethod(lambda cls: None))
69+
@patch("pymisp.PyMISP")
70+
def test_bulk_add_event_called_once(self, mock_pymisp_cls):
71+
"""
72+
run() must call add_event exactly once with all attributes already
73+
attached to the event object. add_attribute on the MISP instance
74+
must never be called (that was the old N+1 pattern).
75+
"""
76+
mock_instance = MagicMock()
77+
mock_pymisp_cls.return_value = mock_instance
78+
79+
mock_event = MagicMock()
80+
mock_event.id = 42
81+
mock_instance.add_event.return_value = mock_event
82+
mock_instance.get_event.return_value = {"Event": {"id": 42}}
83+
84+
job, config, pcs = self._setup_job()
85+
try:
86+
connector = MISP(config)
87+
connector.start(job.pk, {}, uuid())
88+
89+
report = ConnectorReport.objects.get(job=job, config=config)
90+
self.assertEqual(report.status, ConnectorReport.STATUSES.SUCCESS)
91+
92+
# Core assertion: single bulk call, never individual add_attribute
93+
mock_instance.add_event.assert_called_once()
94+
mock_instance.add_attribute.assert_not_called()
95+
96+
# The event passed to add_event must already carry the attributes
97+
event_arg = mock_instance.add_event.call_args[0][0]
98+
self.assertGreaterEqual(
99+
len(event_arg.attributes),
100+
1,
101+
"Event must have at least the base attribute attached before add_event",
102+
)
103+
finally:
104+
self._cleanup(job, config, pcs)
105+
106+
@patch.object(MISP, "_monkeypatch", classmethod(lambda cls: None))
107+
@patch("pymisp.PyMISP")
108+
def test_all_attributes_present_on_event(self, mock_pymisp_cls):
109+
"""
110+
The event sent to MISP must contain the base attribute and the
111+
link attribute — all in one shot.
112+
"""
113+
mock_instance = MagicMock()
114+
mock_pymisp_cls.return_value = mock_instance
115+
116+
mock_event = MagicMock()
117+
mock_event.id = 99
118+
mock_instance.add_event.return_value = mock_event
119+
mock_instance.get_event.return_value = {"Event": {"id": 99}}
120+
121+
job, config, pcs = self._setup_job()
122+
try:
123+
connector = MISP(config)
124+
connector.start(job.pk, {}, uuid())
125+
126+
event_arg = mock_instance.add_event.call_args[0][0]
127+
attr_types = [a.type for a in event_arg.attributes]
128+
129+
# base observable attribute
130+
self.assertIn("ip-src", attr_types)
131+
# link back to IntelOwl job
132+
self.assertIn("link", attr_types)
133+
finally:
134+
self._cleanup(job, config, pcs)
135+
136+
@patch.object(MISP, "_monkeypatch", classmethod(lambda cls: None))
137+
@patch("pymisp.PyMISP")
138+
def test_add_event_failure_marks_report_failed(self, mock_pymisp_cls):
139+
"""
140+
If add_event raises, the connector report status must be FAILED,
141+
not silently swallowed.
142+
"""
143+
mock_instance = MagicMock()
144+
mock_pymisp_cls.return_value = mock_instance
145+
mock_instance.add_event.side_effect = Exception("MISP unreachable")
146+
147+
job, config, pcs = self._setup_job()
148+
try:
149+
connector = MISP(config)
150+
try:
151+
connector.start(job.pk, {}, uuid())
152+
except Exception:
153+
pass
154+
155+
report = ConnectorReport.objects.get(job=job, config=config)
156+
self.assertEqual(report.status, ConnectorReport.STATUSES.FAILED)
157+
finally:
158+
self._cleanup(job, config, pcs)

0 commit comments

Comments
 (0)