def _create_or_update_indicator(self, indicator: Indicator): """ Creates or updates a STIX-2 Indicator in OpenCTI @param indicator The STIX-2 Indicator """ ioc_dct = json.loads(indicator.serialize()) ioc_dct["name"] = ioc_dct.get("name", indicator.id) # default to UUID ioc_dct["stix_id"] = indicator.id del ioc_dct["id"] obs_type = ioc_dct.get("x_opencti_main_observable_type", "Unknown") ioc_dct["x_opencti_main_observable_type"] = obs_type resp = self.opencti_helper.api.indicator.create(**ioc_dct) self.opencti_helper.log_info(f"Created or added to indicator: {resp}")
def test_zmq_app_plugin_message_roundtrip(self): """ Backend-agnostic message passing scenario. Sends a fixed amount of messages via the threatbus ZeroMQ app plugin, subscribes to Threat Bus, and checks if the initially sent messages can be retrieved back. """ result_q = queue.Queue() items = 2 topics = ["stix2/indicator", "stix2/sighting"] rec = threading.Thread(target=zmq_receiver.forward, args=(items, topics, result_q), daemon=False) rec.start() ioc = Indicator(pattern_type="stix", pattern="[ipv4-addr:value = '6.6.6.6']") zmq_sender.send( "stix2/indicator", ioc.serialize(), port=13372, bind=False, ) sighting = Sighting(sighting_of_ref=ioc.id) zmq_sender.send( "stix2/sighting", sighting.serialize(), port=13372, bind=False, ) time.sleep(1) self.assertEqual(result_q.qsize(), items) event = result_q.get(timeout=1) self.assertIsNotNone(event) self.assertEqual(parse(event), ioc) result_q.task_done() event = result_q.get(timeout=1) self.assertIsNotNone(event) self.assertEqual(parse(event), sighting) result_q.task_done() self.assertEqual(0, result_q.qsize()) result_q.join() rec.join(timeout=1)
def _ingest_indicator(self, indicator: Indicator): """ Ingests a STIX-2 Indicator into OpenCTI. Does nothing in case the indicator already exists. @param indicator The STIX-2 Indicator object to ingest """ if type(indicator) is not Indicator: self.opencti_helper.log_error( f"Error ingesting indicator from Threat Bus. Expected a STIX-2 Indicator: {indicator}" ) return ioc_dct = json.loads(indicator.serialize()) ioc_dct["name"] = ioc_dct.get("name", indicator.id) # default to UUID ioc_dct["stix_id"] = indicator.id del ioc_dct["id"] obs_type = ioc_dct.get("x_opencti_main_observable_type", "Unknown") ioc_dct["x_opencti_main_observable_type"] = obs_type resp = self.opencti_helper.api.indicator.create(**ioc_dct) self.opencti_helper.log_info(f"Created or added to indicator: {resp}")
import zmq import time from stix2 import Indicator, Sighting def send(topic, msg, host="127.0.0.1", port=50000, bind=True): """Sends a single, user specified message""" socket = zmq.Context().socket(zmq.PUB) if bind is True: socket.bind(f"tcp://{host}:{port}") time.sleep(0.5) else: socket.connect(f"tcp://{host}:{port}") time.sleep(0.5) # print(f"send string: {topic} {msg}") socket.send_string(f"{topic} {msg}") time.sleep(0.5) if __name__ == "__main__": indicator = Indicator( pattern="[domain-name:value = 'evil.com']", pattern_type="stix" ) sighting = Sighting( sighting_of_ref="indicator--629a6400-8817-4bcb-aee7-8c74fc57482c", custom_properties={"x_threatbus_source": "VAST"}, ) send("stix2/indicator", indicator.serialize(), port=13372, bind=False) send("stix2/sighting", sighting.serialize(), port=13372, bind=False)
class TestMessageMapping(unittest.TestCase): def setUp(self): self.ts = datetime.now(timezone.utc).astimezone() self.indicator_id = "indicator--de0c3d3f-02ee-4086-88f1-51200ac831f7" self.point_ioc = "evil.com" self.pattern = f"[domain-name:value = '{self.point_ioc}']" self.indicator = Indicator( id=self.indicator_id, created=self.ts, modified=self.ts, pattern_type="stix", pattern=self.pattern, ) self.module_namespace = "TestNamespace" self.logger = getLogger("test") def test_invalid_indicator_inputs(self): self.assertIsNone( map_indicator_to_broker_event(None, None, self.logger)) self.assertIsNone(map_indicator_to_broker_event(None, "", self.logger)) self.assertIsNone( map_indicator_to_broker_event(None, self.module_namespace, self.logger)) self.assertIsNone( map_indicator_to_broker_event(42, self.module_namespace, self.logger)) self.assertIsNone( map_indicator_to_broker_event(object, self.module_namespace, self.logger)) self.assertIsNone( map_indicator_to_broker_event( Sighting(sighting_of_ref=self.indicator_id), self.module_namespace, self.logger, )) def test_invalid_zeek_inputs(self): broker_data = broker.zeek.Event("Hello") # unknown event self.assertIsNone( map_broker_event_to_sighting(broker_data, None, self.logger)) self.assertIsNone( map_broker_event_to_sighting(broker_data, self.module_namespace, self.logger)) self.assertIsNone( map_management_message(broker_data, None, self.logger)) self.assertIsNone( map_management_message(broker_data, self.module_namespace, self.logger)) # not enough arguments provided broker_data = broker.zeek.Event("sighting", 1, 2) self.assertIsNone( map_broker_event_to_sighting(broker_data, None, self.logger)) self.assertIsNone( map_broker_event_to_sighting(broker_data, self.module_namespace, self.logger)) self.assertIsNone( map_management_message(broker_data, None, self.logger)) self.assertIsNone( map_management_message(broker_data, self.module_namespace, self.logger)) broker_data = broker.zeek.Event("intel", 42, {}) self.assertIsNone( map_broker_event_to_sighting(broker_data, None, self.logger)) self.assertIsNone( map_broker_event_to_sighting(broker_data, self.module_namespace, self.logger)) self.assertIsNone( map_management_message(broker_data, None, self.logger)) self.assertIsNone( map_management_message(broker_data, self.module_namespace, self.logger)) broker_data = broker.zeek.Event("subscribe", "topic") self.assertIsNone( map_broker_event_to_sighting(broker_data, None, self.logger)) self.assertIsNone( map_broker_event_to_sighting(broker_data, self.module_namespace, self.logger)) self.assertIsNone( map_management_message(broker_data, None, self.logger)) self.assertIsNone( map_management_message(broker_data, self.module_namespace, self.logger)) broker_data = broker.zeek.Event("unsubscribe") self.assertIsNone( map_broker_event_to_sighting(broker_data, None, self.logger)) self.assertIsNone( map_broker_event_to_sighting(broker_data, self.module_namespace, self.logger)) self.assertIsNone( map_management_message(broker_data, None, self.logger)) self.assertIsNone( map_management_message(broker_data, self.module_namespace, self.logger)) def test_valid_indicator(self): # test indicator added broker_msg = map_indicator_to_broker_event(self.indicator, self.module_namespace, None) self.assertEqual(broker_msg.name(), self.module_namespace + "::intel") self.assertEqual( broker_msg.args(), [(self.ts, self.indicator_id, "DOMAIN", self.point_ioc, "ADD")], ) # test indicator removed # deep copy indicator, add custom property that indicates deletion i_dct = json.loads(self.indicator.serialize()) # deep copy i_dct[ThreatBusSTIX2Constants.X_THREATBUS_UPDATE. value] = Operation.REMOVE.value indicator_copy = parse(json.dumps(i_dct), allow_custom=True) broker_msg = map_indicator_to_broker_event(indicator_copy, self.module_namespace, None) self.assertEqual(broker_msg.name(), self.module_namespace + "::intel") self.assertEqual( broker_msg.args(), [(self.ts, self.indicator_id, "DOMAIN", self.point_ioc, "REMOVE")], ) def test_valid_zeek_sighting(self): context = {"last_seen": 1234, "count": 13, "source": "Zeek"} # without namespace: event = broker.zeek.Event("sighting", self.ts, self.indicator_id, context) sighting = map_broker_event_to_sighting(event, self.module_namespace, None) self.assertEqual(type(sighting), Sighting) self.assertEqual(sighting.last_seen, self.ts) self.assertEqual(sighting.sighting_of_ref, self.indicator_id) self.assertTrue(ThreatBusSTIX2Constants.X_THREATBUS_SIGHTING_CONTEXT. value in sighting) self.assertEqual(sighting.x_threatbus_sighting_context, context) # with namespace: event = broker.zeek.Event(self.module_namespace + "::sighting", self.ts, self.indicator_id, context) sighting = map_broker_event_to_sighting(event, self.module_namespace, None) self.assertEqual(type(sighting), Sighting) self.assertEqual(sighting.last_seen, self.ts) self.assertEqual(sighting.sighting_of_ref, self.indicator_id) self.assertTrue(ThreatBusSTIX2Constants.X_THREATBUS_SIGHTING_CONTEXT. value in sighting) self.assertEqual(sighting.x_threatbus_sighting_context, context) def test_valid_subscription(self): td = timedelta(days=5) topic = "some/topic" expected = Subscription(topic, td) # without namespace event = broker.zeek.Event("subscribe", topic, td) subscription = map_management_message(event, self.module_namespace, self.logger) self.assertEqual(subscription, expected) # with namespace: event = broker.zeek.Event(self.module_namespace + "::subscribe", topic, td) subscription = map_management_message(event, self.module_namespace, self.logger) self.assertEqual(subscription, expected) def test_valid_unsubscription(self): topic = "some/topic" expected = Unsubscription(topic) # without namespace event = broker.zeek.Event("unsubscribe", topic) unsubscription = map_management_message(event, self.module_namespace, self.logger) self.assertEqual(unsubscription, expected) # with namespace: event = broker.zeek.Event(self.module_namespace + "::unsubscribe", topic) unsubscription = map_management_message(event, self.module_namespace, self.logger) self.assertEqual(unsubscription, expected)
def test_intel_sighting_roundtrip(self): """ Backend-agnostic roundtrip scenario, that starts a Zeek subprocess which activates the threatbus.zeek "app" script. The test sends an IoC with a malicious hostname via Threat Bus, using the ZMQ app plugin. Meanwhile, the Zeek subprocess reads a PCAP trace which contains exactly that malicious hostname from the IoC. If all goes well, Zeek subscribes to Threat Bus successfully, receives the IoC and hence reading the PCAP file results in a sighting. Zeek forwards that sighting to the Threat Bus Zeek plugin, where it is converted to a valid STIX-2 Sighting. The integration test subscribes a ZMQ receiver to the `stix2/sighting` topic and verifies all Zeek communication was handled correctly. I.e., Zeek matched the IoC and reported the correct sighting. """ # Start a ZMQ receiver that subscribes to the `stix2/sighting` topic and # forward exactly 1 item to a result queue result_q = queue.Queue() rec = threading.Thread( target=zmq_receiver.forward, args=(1, ["stix2/sighting"], result_q), daemon=True, ) rec.start() # Spawn a Zeek subprocess that runs the `apps/zeek/threatbus.zeek` # script and reads a prepared PCAP trace that contains a network # connection to `example.com` zeek_process = RunZeek() if not zeek_process: self.fail("Error starting Zeek container.") # Let Zeek start up... time.sleep(1) # Send a new indicator (IoC) via the ZMQ test-util, which will be # forwarded to Zeek because Zeek subscribes to `stix2/indicator` ioc_id = "indicator--42d31a5b-2da0-4bdd-9823-1723a98fc2fb" ioc = Indicator( id=ioc_id, pattern_type="stix", pattern="[domain-name:value = 'example.com']", ) zmq_sender.send("stix2/indicator", ioc.serialize(), port=13372, bind=False) # Wait for Zeek to ingest the IoC into its Intel framework, read the # PCAP trace and report back the sighting raw_msg = result_q.get(timeout=10) sighting = parse(raw_msg, allow_custom=True) result_q.task_done() self.assertIsNotNone(sighting) self.assertEqual(sighting.sighting_of_ref, ioc_id) self.assertTrue(ThreatBusSTIX2Constants.X_THREATBUS_SIGHTING_CONTEXT. value in sighting) self.assertEqual(sighting.x_threatbus_sighting_context, {"noisy": False}) rec.join() result_q.join() zeek_process.kill() self.assertTrue(StopZeek())
import pika from stix2 import Indicator ## Dummy intel data pattern = "[ipv4-addr:value = '6.6.6.6']" pattern_type = "stix2" indicator = Indicator(pattern=pattern, pattern_type=pattern_type) indicator_json = indicator.serialize() ## rabbitmq host = "localhost" port = "5672" vhost = "/" credentials = pika.PlainCredentials("guest", "guest") conn_params = pika.ConnectionParameters(host, port, vhost, credentials) connection = pika.BlockingConnection(conn_params) channel = connection.channel() for i in range(100): channel.basic_publish(exchange="threatbus", routing_key="", body=indicator_json)