import demistomock as demisto from CommonServerPython import * ''' IMPORTS ''' import requests from pykafka import KafkaClient, SslConfig from pykafka.common import OffsetType import logging from cStringIO import StringIO import traceback # Disable insecure warnings requests.packages.urllib3.disable_warnings() ''' GLOBALS/PARAMS ''' # Logging log_stream = None log_handler = None ''' HELPER FUNCTIONS ''' def start_logging(): logging.raiseExceptions = False global log_stream global log_handler if log_stream is None: log_stream = StringIO() log_handler = logging.StreamHandler(stream=log_stream) log_handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) logger = logging.getLogger() logger.addHandler(log_handler) logger.setLevel(logging.DEBUG) def check_params(topic, old_offset=None, old_partition=None): """ :param topic: topic to check :type topic: :class: `pykafka.topic.Topic` :param offset: offset to check if is in the topic (and cast it to in if needed) :type offset: int, str, unicode or None :param partition: partition to check if is in the topic (and cast to int if needed) :type partition: int or str or unicode or None :returns: new_offset, new_partition :rtype: int, int """ partition = None offset = None # Casting if old_partition: # Casting if isinstance(old_partition, (unicode, str)): if old_partition.isdigit(): partition = int(old_partition) else: return_error('Supplied partition is not a number') if isinstance(old_partition, int): partition = old_partition if old_offset: # Casting if isinstance(old_offset, (unicode, str)): if old_offset.isdigit(): offset = int(old_offset) offset = OffsetType.EARLIEST if offset == 0 else offset - 1 elif old_offset.lower() == 'earliest': offset = OffsetType.EARLIEST elif old_offset.lower() == 'latest': offset = check_latest_offset(topic, partition_number=partition) - 1 else: return_error('Supplied offset is not a number') if check_latest_offset(topic, partition_number=partition) <= offset: return_error('Offset is out of bounds') else: return_error('Offset is not a number, earliest or latest') return offset, partition def create_incident(message, topic): """ Creates incident :param message: Kafka message to create incident from :type message: :class:`pykafka.common.Message` :param topic: Message's topic :type topic: str :return incident: """ raw = { 'Topic': topic, 'Partition': message.partition_id, 'Offset': message.offset, 'Message': message.value } incident = { 'name': 'Kafka {} partition:{} offset:{}'.format(topic, message.partition_id, message.offset), 'details': message.value, 'rawJSON': json.dumps(raw) } if message.timestamp_dt: incident['occurred'] = message.timestamp_dt return incident def check_latest_offset(topic, partition_number=None): """ :param topic: topic to check the latest offset :type topic: :class:`pykafka.topic.Topic` :param partition_number: partition to take latest offset from :type partition_number: int, str :return latest_offset: last message offset :rtype: int """ partitions = topic.latest_available_offsets() latest_offset = 0 if partition_number is not None: partition = partitions.get(str(partition_number)) if partitions: latest_offset = partition[0][0] else: return_error('Partition does not exist') else: for partition in partitions.values(): if latest_offset < partition[0][0]: latest_offset = partition[0][0] return latest_offset - 1 def create_certificate(ca_cert=None, client_cert=None, client_cert_key=None, password=None): """ Creating certificate :return certificate: :return type: :class: `pykafka.connection.SslConfig` """ ca_path = None client_path = None client_key_path = None if ca_cert: ca_path = 'ca.cert' # type: ignore with open(ca_path, 'wb') as file: file.write(ca_cert) ca_path = os.path.abspath(ca_path) if client_cert: client_path = 'client.cert' with open(client_path, 'wb') as file: file.write(client_cert) client_path = os.path.abspath(client_path) if client_cert_key: client_key_path = 'client_key.key' with open(client_key_path, 'wb') as file: file.write(client_cert_key) return SslConfig( cafile=ca_path, certfile=client_path, keyfile=client_key_path, password=password ) ''' COMMANDS + REQUESTS FUNCTIONS ''' def test_module(client): """ If we got here, the instance is working without any error """ if client.topics is not None: demisto.results('ok') def print_topics(client): """ Prints available topics in Broker """ include_offsets = demisto.args().get('include_offsets', 'true') == 'true' kafka_topics = client.topics.values() if kafka_topics: topics = [] for topic in kafka_topics: partitions = [] for partition in topic.partitions.values(): partition_output = {'ID': partition.id} if include_offsets: try: partition_output['EarliestOffset'] = partition.earliest_available_offset() partition_output['OldestOffset'] = partition.latest_available_offset() except Exception as e: demisto.error('Failed fetching available offset for topic {} partition {} - {}'.format( topic.name, partition.id, str(e)) ) partitions.append(partition_output) topics.append({ 'Name': topic.name, 'Partitions': partitions }) ec = { 'Kafka.Topic(val.Name === obj.Name)': topics } md = tableToMarkdown('Kafka Topics', topics) demisto.results({ 'Type': entryTypes['note'], 'Contents': topics, 'ContentsFormat': formats['json'], 'HumanReadable': md, 'ReadableContentsFormat': formats['markdown'], 'EntryContext': ec }) else: demisto.results('No topics found.') def produce_message(client): """ Producing message to kafka topic """ topic = demisto.args().get('topic') value = demisto.args().get('value') partitioning_key = demisto.args().get('partitioning_key') partitioning_key = str(partitioning_key) if partitioning_key.isdigit(): partitioning_key = int(partitioning_key) # type: ignore else: partitioning_key = None # type: ignore if topic in client.topics: kafka_topic = client.topics[topic] with kafka_topic.get_sync_producer() as producer: producer.produce( message=str(value), partition_key=partitioning_key ) demisto.results('Message was successfully produced to topic \'{}\''.format(topic)) else: return_error('Topic {} was not found in Kafka'.format(topic)) def consume_message(client): """ Consuming one message from topic """ topic = demisto.args().get('topic') offset = demisto.args().get('offset') partition = demisto.args().get('partition') if topic in client.topics: kafka_topic = client.topics[topic] offset, partition = check_params(kafka_topic, old_offset=offset, old_partition=partition) consumer = kafka_topic.get_simple_consumer( auto_offset_reset=offset, reset_offset_on_start=True ) message = consumer.consume() md = tableToMarkdown( name='Message consumed from topic \'{}\''.format(topic), t={ 'Offset': message.offset, 'Message': message.value }, headers=[ 'Offset', 'Message' ] ) ec = { 'Kafka.Topic(val.Name === obj.Name)': { 'Name': topic, 'Message': { 'Value': message.value, 'Offset': message.offset } } } demisto.results({ 'Type': entryTypes['note'], 'Contents': { 'Message': message.value, 'Offset': message.offset }, 'ContentsFormat': formats['json'], 'HumanReadable': md, 'ReadableContentsFormat': formats['markdown'], 'EntryContext': ec }) else: return_error('Topic {} was not found in Kafka'.format(topic)) def fetch_partitions(client): """ Fetching available partitions in given topic """ topic = demisto.args().get('topic') if topic in client.topics: kafka_topic = client.topics[topic] partitions = kafka_topic.partitions.keys() md = tableToMarkdown( name='Available partitions for topic \'{}\''.format(topic), t=partitions, headers='Partitions' ) ec = { 'Kafka.Topic(val.Name === obj.Name)': { 'Name': topic, 'Partition': partitions } } contents = { topic: partitions } demisto.results({ 'Type': entryTypes['note'], 'Contents': contents, 'ContentsFormat': formats['json'], 'HumanReadable': md, 'ReadableContentsFormat': formats['markdown'], 'EntryContext': ec }) else: return_error('Topic {} was not found in Kafka'.format(topic)) def fetch_incidents(client): """ Fetches incidents """ topic = demisto.params().get('topic', '') partition_to_fetch_from = argToList(demisto.params().get('partition', '')) offset_to_fetch_from = demisto.params().get('offset', -2) message_max_bytes = int(demisto.params().get("max_bytes_per_message", 1048576)) try: offset_to_fetch_from = int(offset_to_fetch_from) except ValueError as e: demisto.error('Received invalid offset: {}. Using default of -2. Err: {}'.format(offset_to_fetch_from, e)) offset_to_fetch_from = -2 max_messages = demisto.params().get('max_messages', 50) try: max_messages = int(max_messages) except ValueError: max_messages = 50 last_fetched_partitions_offset = json.loads(demisto.getLastRun().get('last_fetched_partitions_offset', '{}')) incidents = [] message_counter = 0 if topic in client.topics: kafka_topic = client.topics[topic] consumer_args = { 'consumer_timeout_ms': 2000, # wait max 2 seconds for new messages 'reset_offset_on_start': True, 'auto_offset_reset': offset_to_fetch_from, 'fetch_message_max_bytes': message_max_bytes } if partition_to_fetch_from: partitions = [] for partition in kafka_topic.partitions.values(): partition_id = str(partition.id) if partition_id in partition_to_fetch_from: partitions.append(partition) consumer_args['partitions'] = partitions # type: ignore consumer = kafka_topic.get_simple_consumer(**consumer_args) offsets = [(p, last_fetched_partitions_offset.get(str(p.id), offset_to_fetch_from)) for p in consumer._partitions] consumer.reset_offsets(offsets) for message in consumer: if message and message.value: incidents.append(create_incident(message=message, topic=kafka_topic.name)) if message.offset > last_fetched_partitions_offset.get(str(message.partition_id), offset_to_fetch_from): last_fetched_partitions_offset[str(message.partition_id)] = message.offset message_counter += 1 if message_counter == max_messages: break consumer.stop() else: return_error('No such topic \'{}\' to fetch incidents from.'.format(topic)) demisto.setLastRun({'last_fetched_partitions_offset': json.dumps(last_fetched_partitions_offset)}) demisto.incidents(incidents) ''' COMMANDS MANAGER / SWITCH PANEL ''' def main(): LOG('Command being called is {0}'.format(demisto.command())) global log_stream BROKERS = demisto.params().get('brokers') # Should we use SSL USE_SSL = demisto.params().get('use_ssl', False) # Certificates CA_CERT = demisto.params().get('ca_cert', None) CLIENT_CERT = demisto.params().get('client_cert', None) CLIENT_CERT_KEY = demisto.params().get('client_cert_key', None) PASSWORD = demisto.params().get('additional_password', None) try: # Initialize KafkaClient if USE_SSL: ssl_config = create_certificate(CA_CERT, CLIENT_CERT, CLIENT_CERT_KEY, PASSWORD) client = KafkaClient(hosts=BROKERS, ssl_config=ssl_config) else: client = KafkaClient(hosts=BROKERS) if demisto.command() == 'test-module': start_logging() # This is the call made when pressing the integration test button. test_module(client) elif demisto.command() == 'kafka-print-topics': print_topics(client) elif demisto.command() == 'kafka-publish-msg': produce_message(client) elif demisto.command() == 'kafka-consume-msg': consume_message(client) elif demisto.command() == 'kafka-fetch-partitions': fetch_partitions(client) elif demisto.command() == 'fetch-incidents': fetch_incidents(client) except Exception as e: debug_log = 'Debug logs:\n\n{0}'.format(log_stream.getvalue() if log_stream else '') error_message = str(e) if demisto.command() != 'test-module': stacktrace = traceback.format_exc() if stacktrace: debug_log += '\nFull stacktrace:\n\n{0}'.format(stacktrace) return_error('{0}\n\n{1}'.format(error_message, debug_log)) finally: if os.path.isfile('ca.cert'): os.remove(os.path.abspath('ca.cert')) if os.path.isfile('client.cert'): os.remove(os.path.abspath('client.cert')) if os.path.isfile('client_key.key'): os.remove(os.path.abspath('client_key.key')) if log_stream: try: logging.getLogger().removeHandler(log_handler) # type: ignore log_stream.close() log_stream = None except Exception as e: demisto.error('Kafka v2: unexpected exception when trying to remove log handler: {}'.format(e)) if __name__ == "__builtin__" or __name__ == "builtins": main()