Files
DetectionLab/Vagrant/resources/malcolm/shared/bin/configure-capture.py
2021-08-06 10:35:01 +02:00

908 lines
52 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021 Battelle Energy Alliance, LLC. All rights reserved.
# script for configuring sensor capture and forwarding parameters
import locale
import os
import re
import shutil
import sys
import fileinput
from collections import defaultdict
from dialog import Dialog
from zeek_carve_utils import *
from sensorcommon import *
class Constants:
CONFIG_CAP = 'Capture Configuration'
DEV_IDENTIFIER_FILE = '/etc/installer'
DEV_UNKNOWN = 'unknown'
DEV_AGGREGATOR = 'aggregator'
DEV_SENSOR = 'sensor'
DEV_VALID = {DEV_AGGREGATOR, DEV_SENSOR}
MSG_ERR_DEV_INVALID = f'Could not determine installation type (not one of {DEV_VALID})'
MSG_ERR_DEV_INCORRECT = 'This tool is not suitable for configuring {}s'
SENSOR_CAPTURE_CONFIG = '/opt/sensor/sensor_ctl/control_vars.conf'
PCAP_CAPTURE_AUTOSTART_ENTRIES = {'AUTOSTART_ARKIME', 'AUTOSTART_NETSNIFF', 'AUTOSTART_TCPDUMP'}
ZEEK_FILE_CARVING_NONE = 'none'
ZEEK_FILE_CARVING_ALL = 'all'
ZEEK_FILE_CARVING_KNOWN = 'known'
ZEEK_FILE_CARVING_MAPPED = 'mapped'
ZEEK_FILE_CARVING_MAPPED_MINUS_TEXT = 'mapped (except common plain text files)'
ZEEK_FILE_CARVING_INTERESTING = 'interesting'
ZEEK_FILE_CARVING_CUSTOM = 'custom'
ZEEK_FILE_CARVING_CUSTOM_MIME = 'custom (mime-sorted)'
ZEEK_FILE_CARVING_CUSTOM_EXT = 'custom (extension-sorted)'
ZEEK_FILE_CARVING_DEFAULTS = '/opt/zeek/share/zeek/site/extractor_params.zeek'
ZEEK_FILE_CARVING_OVERRIDE_FILE = '/opt/sensor/sensor_ctl/extractor_override.zeek'
ZEEK_FILE_CARVING_OVERRIDE_INTERESTING_FILE = '/opt/sensor/sensor_ctl/extractor_override.interesting.zeek'
ZEEK_FILE_CARVING_OVERRIDE_FILE_MAP_NAME = 'extractor_mime_to_ext_map'
ZEEK_FILE_CARVING_PLAIN_TEXT_MIMES = {
"application/json",
"application/x-x509-ca-cert",
"application/xml",
"text/plain",
"text/xml"
}
FILEBEAT='filebeat'
METRICBEAT='metricbeat'
AUDITBEAT='auditbeat'
HEATBEAT='heatbeat' # protologbeat to log temperature and other misc. stuff
SYSLOGBEAT='filebeat-syslog' # another filebeat instance for syslog
ARKIMECAP='moloch-capture'
BEAT_DIR = {
FILEBEAT : f'/opt/sensor/sensor_ctl/{FILEBEAT}',
METRICBEAT : f'/opt/sensor/sensor_ctl/{METRICBEAT}',
AUDITBEAT : f'/opt/sensor/sensor_ctl/{AUDITBEAT}',
SYSLOGBEAT : f'/opt/sensor/sensor_ctl/{SYSLOGBEAT}',
HEATBEAT : f'/opt/sensor/sensor_ctl/{HEATBEAT}'
}
BEAT_KIBANA_DIR = {
FILEBEAT : f'/usr/share/{FILEBEAT}/kibana',
METRICBEAT : f'/usr/share/{METRICBEAT}/kibana',
AUDITBEAT : f'/usr/share/{AUDITBEAT}/kibana',
SYSLOGBEAT : f'/usr/share/{FILEBEAT}/kibana',
HEATBEAT : f'/usr/share/protologbeat/kibana/'
}
BEAT_CMD = {
FILEBEAT : f'{FILEBEAT} --path.home "{BEAT_DIR[FILEBEAT]}" --path.config "{BEAT_DIR[FILEBEAT]}" --path.data "{BEAT_DIR[FILEBEAT]}/data" --path.logs "{BEAT_DIR[FILEBEAT]}/logs" -c "{BEAT_DIR[FILEBEAT]}/{FILEBEAT}.yml"',
METRICBEAT : f'{METRICBEAT} --path.home "{BEAT_DIR[METRICBEAT]}" --path.config "{BEAT_DIR[METRICBEAT]}" --path.data "{BEAT_DIR[METRICBEAT]}/data" --path.logs "{BEAT_DIR[METRICBEAT]}/logs" -c "{BEAT_DIR[METRICBEAT]}/{METRICBEAT}.yml"',
AUDITBEAT : f'{AUDITBEAT} --path.home "{BEAT_DIR[AUDITBEAT]}" --path.config "{BEAT_DIR[AUDITBEAT]}" --path.data "{BEAT_DIR[AUDITBEAT]}/data" --path.logs "{BEAT_DIR[AUDITBEAT]}/logs" -c "{BEAT_DIR[AUDITBEAT]}/{AUDITBEAT}.yml"',
SYSLOGBEAT : f'{FILEBEAT} --path.home "{BEAT_DIR[SYSLOGBEAT]}" --path.config "{BEAT_DIR[SYSLOGBEAT]}" --path.data "{BEAT_DIR[SYSLOGBEAT]}/data" --path.logs "{BEAT_DIR[SYSLOGBEAT]}/logs" -c "{BEAT_DIR[SYSLOGBEAT]}/{SYSLOGBEAT}.yml"',
HEATBEAT : f'protologbeat --path.home "{BEAT_DIR[HEATBEAT]}" --path.config "{BEAT_DIR[HEATBEAT]}" --path.data "{BEAT_DIR[HEATBEAT]}/data" --path.logs "{BEAT_DIR[HEATBEAT]}/logs" -c "{BEAT_DIR[HEATBEAT]}/protologbeat.yml"'
}
# specific to beats forwarded to logstash (eg., filebeat)
BEAT_LS_HOST = 'BEAT_LS_HOST'
BEAT_LS_PORT = 'BEAT_LS_PORT'
BEAT_LS_SSL = 'BEAT_LS_SSL'
BEAT_LS_SSL_CA_CRT = 'BEAT_LS_SSL_CA_CRT'
BEAT_LS_SSL_CLIENT_CRT = 'BEAT_LS_SSL_CLIENT_CRT'
BEAT_LS_SSL_CLIENT_KEY = 'BEAT_LS_SSL_CLIENT_KEY'
BEAT_LS_SSL_VERIFY = 'BEAT_LS_SSL_VERIFY'
# specific to beats forwarded to elasticsearch (eg., metricbeat, auditbeat, filebeat-syslog)
BEAT_ES_HOST = "BEAT_ES_HOST"
BEAT_ES_PORT = "BEAT_ES_PORT"
BEAT_ES_PROTOCOL = "BEAT_ES_PROTOCOL"
BEAT_ES_SSL_VERIFY = "BEAT_ES_SSL_VERIFY"
BEAT_HTTP_PASSWORD = "BEAT_HTTP_PASSWORD"
BEAT_HTTP_USERNAME = "BEAT_HTTP_USERNAME"
BEAT_KIBANA_DASHBOARDS_ENABLED = "BEAT_KIBANA_DASHBOARDS_ENABLED"
BEAT_KIBANA_DASHBOARDS_PATH = "BEAT_KIBANA_DASHBOARDS_PATH"
BEAT_KIBANA_HOST = "BEAT_KIBANA_HOST"
BEAT_KIBANA_PORT = "BEAT_KIBANA_PORT"
BEAT_KIBANA_PROTOCOL = "BEAT_KIBANA_PROTOCOL"
BEAT_KIBANA_SSL_VERIFY = "BEAT_KIBANA_SSL_VERIFY"
# specific to filebeat
BEAT_LOG_PATH_SUBDIR = os.path.join('logs', 'current')
BEAT_LOG_PATTERN_KEY = 'BEAT_LOG_PATTERN'
BEAT_LOG_PATTERN_VAL = '*.log'
# specific to metricbeat
BEAT_INTERVAL = "BEAT_INTERVAL"
# specific to moloch
ARKIME_PACKET_ACL = "ARKIME_PACKET_ACL"
MSG_CONFIG_MODE = 'Configuration Mode'
MSG_CONFIG_MODE_CAPTURE = 'Configure Capture'
MSG_CONFIG_MODE_FORWARD = 'Configure Forwarding'
MSG_CONFIG_MODE_AUTOSTART = 'Configure Autostart Services'
MSG_CONFIG_GENERIC = 'Configure {}'
MSG_CONFIG_ARKIME = (f'{ARKIMECAP}', f'Configure Arkime session forwarding via {ARKIMECAP}')
MSG_CONFIG_FILEBEAT = (f'{FILEBEAT}', f'Configure Zeek log forwarding via {FILEBEAT}')
MSG_CONFIG_METRICBEAT = (f'{METRICBEAT}', f'Configure resource metrics forwarding via {METRICBEAT}')
MSG_CONFIG_AUDITBEAT = (f'{AUDITBEAT}', f'Configure audit log forwarding via {AUDITBEAT}')
MSG_CONFIG_SYSLOGBEAT = (f'{SYSLOGBEAT}', f'Configure syslog forwarding via {FILEBEAT}')
MSG_CONFIG_HEATBEAT = (f'{HEATBEAT}', f'Configure hardware metrics (temperature, etc.) forwarding via protologbeat')
MSG_OVERWRITE_CONFIG = '{} is already configured, overwrite current settings?'
MSG_IDENTIFY_NICS = 'Do you need help identifying network interfaces?'
MSG_BACKGROUND_TITLE = 'Sensor Configuration'
MSG_CONFIG_AUTOSTARTS = 'Specify autostart processes'
MSG_CONFIG_ZEEK_CARVED_SCANNERS = 'Specify scanners for Zeek-carved files'
MSG_CONFIG_ZEEK_CARVING = 'Specify Zeek file carving mode'
MSG_CONFIG_ZEEK_CARVING_MIMES = 'Specify file types to carve'
MSG_CONFIG_CARVED_FILE_PRESERVATION = 'Specify which carved files to preserve'
MSG_CONFIG_CAP_CONFIRM = 'Sensor will capture traffic with the following parameters:\n\n{}'
MSG_CONFIG_AUTOSTART_CONFIRM = 'Sensor autostart the following services:\n\n{}'
MSG_CONFIG_FORWARDING_CONFIRM = '{} will forward with the following parameters:\n\n{}'
MSG_CONFIG_CAP_PATHS = 'Provide paths for captured PCAPs and Zeek logs'
MSG_CONFIG_CAPTURE_SUCCESS = 'Capture interface set to {} in {}.\n\nReboot to apply changes.'
MSG_CONFIG_AUTOSTART_SUCCESS = 'Autostart services configured.\n\nReboot to apply changes.'
MSG_CONFIG_FORWARDING_SUCCESS = '{} forwarding configured:\n\n{}\n\nRestart forwarding services or reboot to apply changes.'
MSG_CONFIG_ARKIME_PCAP_ACL = 'Specify IP addresses for PCAP retrieval ACL (one per line)'
MSG_ERR_PLEBE_REQUIRED = 'this utility should be be run as non-privileged user'
MSG_ERROR_DIR_NOT_FOUND = 'One or more of the paths specified does not exist'
MSG_ERROR_FILE_NOT_FOUND = 'One or more of the files specified does not exist'
MSG_ERROR_BAD_HOST = 'Invalid host or port'
MSG_ERROR_FWD_DIR_NOT_FOUND = 'The path {} does not exist, {} cannot be configured'
MSG_ERROR_MISSING_CAP_CONFIG = f'Capture configuration file {SENSOR_CAPTURE_CONFIG} does not exist'
MSG_ERROR_KEYSTORE = 'There was an error creating the keystore for {}:\n\n{}'
MSG_ERROR_FILTER_VALIDATION = "Warning: capture filter failed validation ({}). Adjust filter, or resubmit unchanged to ignore warning."
MSG_MESSAGE_ERROR = 'Error: {}\n\nPlease try again.'
MSG_CANCEL_ERROR = 'Operation cancelled, goodbye!'
MSG_EMPTY_CONFIG_ERROR = "No configuration values were supplied"
MSG_SELECT_INTERFACE = 'Select capture interface(s)'
MSG_SELECT_BLINK_INTERFACE = 'Select capture interface to identify'
MSG_BLINK_INTERFACE = '{} will blink for {} seconds'
MSG_WELCOME_TITLE = 'Welcome to the sensor capture and forwarding configuration utility!'
MSG_TESTING_CONNECTION = 'Testing {} connection...'
MSG_TESTING_CONNECTION_SUCCESS = '{} connection succeeded! ({} {})'
MSG_TESTING_CONNECTION_FAILURE = "{} connection error: {} {}:\n\n {}"
MSG_TESTING_CONNECTION_FAILURE_LOGSTASH = "{} connection error: could not connect to {}:{}"
MSG_WARNING_MULTIPLE_PCAP = "Warning: multiple PCAP processes are enabled ({}). Using a single PCAP process is recommended."
# the main dialog window used for the duration of this tool
d = Dialog(dialog='dialog', autowidgetsize=True)
d.set_background_title(Constants.MSG_BACKGROUND_TITLE)
###################################################################################################
def mime_to_extension_mappings(mapfile):
# get all mime-to-extension mappings from our mapping zeek file into a dictionary
mime_maps = defaultdict(str)
if os.path.exists(mapfile):
maps_list = []
with open(mapfile) as f:
maps_list = [x.replace(' ', '') for x in re.findall(r'(\[\s*"[A-Za-z0-9/\.\+_-]+"\s*\]\s*=\s*"[A-Za-z0-9\.\+_-]+")', f.read(), re.MULTILINE)]
mime_map_re = re.compile(r'\[\s*"([A-Za-z0-9/\.\+_-]+)"\s*\]\s*=\s*"([A-Za-z0-9\.\+_-]+)"')
for mime_map in maps_list:
match = mime_map_re.search(mime_map)
if match:
mime_maps[match.group(1)] = match.group(2)
return mime_maps
###################################################################################################
def input_elasticsearch_connection_info(forwarder,
default_es_host=None,
default_es_port=None,
default_kibana_host=None,
default_kibana_port=None,
default_username=None,
default_password=None):
return_dict = defaultdict(str)
# Elasticsearch configuration
# elasticsearch protocol and SSL verification mode
elastic_protocol = "http"
elastic_ssl_verify = "none"
if (d.yesno("Elasticsearch connection protocol", yes_label="HTTPS", no_label="HTTP") == Dialog.OK):
elastic_protocol = "https"
if (d.yesno("Elasticsearch SSL verification", yes_label="None", no_label="Full") != Dialog.OK):
elastic_ssl_verify = "full"
return_dict[Constants.BEAT_ES_PROTOCOL] = elastic_protocol
return_dict[Constants.BEAT_ES_SSL_VERIFY] = elastic_ssl_verify
while True:
# host/port for Elasticsearch
code, values = d.form(Constants.MSG_CONFIG_GENERIC.format(forwarder), [
('Elasticsearch Host', 1, 1, default_es_host or "", 1, 25, 30, 255),
('Elasticsearch Port', 2, 1, default_es_port or "9200", 2, 25, 6, 5)
])
values = [x.strip() for x in values]
if (code == Dialog.CANCEL) or (code == Dialog.ESC):
raise CancelledError
elif (len(values[0]) <= 0) or (len(values[1]) <= 0) or (not values[1].isnumeric()):
code = d.msgbox(text=Constants.MSG_ERROR_BAD_HOST)
else:
return_dict[Constants.BEAT_ES_HOST] = values[0]
return_dict[Constants.BEAT_ES_PORT] = values[1]
break
# Kibana configuration (if supported by forwarder)
if (forwarder in Constants.BEAT_KIBANA_DIR.keys()) and (d.yesno(f"Configure {forwarder} Kibana connectivity?") == Dialog.OK):
# elasticsearch protocol and SSL verification mode
kibana_protocol = "http"
kibana_ssl_verify = "none"
if (d.yesno("Kibana connection protocol", yes_label="HTTPS", no_label="HTTP") == Dialog.OK):
kibana_protocol = "https"
if (d.yesno("Kibana SSL verification", yes_label="None", no_label="Full") != Dialog.OK):
kibana_ssl_verify = "full"
return_dict[Constants.BEAT_KIBANA_PROTOCOL] = kibana_protocol
return_dict[Constants.BEAT_KIBANA_SSL_VERIFY] = kibana_ssl_verify
while True:
# host/port for Kibana
code, values = d.form(Constants.MSG_CONFIG_GENERIC.format(forwarder), [
('Kibana Host', 1, 1, default_kibana_host or "", 1, 20, 30, 255),
('Kibana Port', 2, 1, default_kibana_port or "5601", 2, 20, 6, 5)
])
values = [x.strip() for x in values]
if (code == Dialog.CANCEL) or (code == Dialog.ESC):
raise CancelledError
elif (len(values[0]) <= 0) or (len(values[1]) <= 0) or (not values[1].isnumeric()):
code = d.msgbox(text=Constants.MSG_ERROR_BAD_HOST)
else:
return_dict[Constants.BEAT_KIBANA_HOST] = values[0]
return_dict[Constants.BEAT_KIBANA_PORT] = values[1]
break
if (d.yesno(f"Configure {forwarder} Kibana dashboards?") == Dialog.OK):
kibana_dashboards = "true"
else:
kibana_dashboards = "false"
return_dict[Constants.BEAT_KIBANA_DASHBOARDS_ENABLED] = kibana_dashboards
if kibana_dashboards == "true":
while True:
code, values = d.form(Constants.MSG_CONFIG_GENERIC.format(forwarder), [
('Kibana Dashboards Path', 1, 1, Constants.BEAT_KIBANA_DIR[forwarder], 1, 30, 30, 255)
])
values = [x.strip() for x in values]
if (code == Dialog.CANCEL) or (code == Dialog.ESC):
raise CancelledError
elif (len(values[0]) <= 0) or (not os.path.isdir(values[0])):
code = d.msgbox(text=Constants.MSG_ERROR_DIR_NOT_FOUND)
else:
return_dict[Constants.BEAT_KIBANA_DASHBOARDS_PATH] = values[0]
break
server_display_name = "Elasticsearch/Kibana" if Constants.BEAT_KIBANA_HOST in return_dict.keys() else "Elasticsearch"
# HTTP/HTTPS authentication
code, http_username = d.inputbox(f"{server_display_name} HTTP/HTTPS server username", init=default_username)
if (code == Dialog.CANCEL) or (code == Dialog.ESC):
raise CancelledError
return_dict[Constants.BEAT_HTTP_USERNAME] = http_username.strip()
# make them enter the password twice
while True:
code, http_password = d.passwordbox(f"{server_display_name} HTTP/HTTPS server password", insecure=True, init=default_password)
if (code == Dialog.CANCEL) or (code == Dialog.ESC):
raise CancelledError
code, http_password2 = d.passwordbox(f"{server_display_name} HTTP/HTTPS server password (again)", insecure=True, init=default_password if (http_password == default_password) else "")
if (code == Dialog.CANCEL) or (code == Dialog.ESC):
raise CancelledError
if (http_password == http_password2):
return_dict[Constants.BEAT_HTTP_PASSWORD] = http_password.strip()
break
else:
code = d.msgbox(text=Constants.MSG_MESSAGE_ERROR.format("Passwords did not match"))
# test Elasticsearch connection
code = d.infobox(Constants.MSG_TESTING_CONNECTION.format("Elasticsearch"))
retcode, message, output = test_connection(protocol=return_dict[Constants.BEAT_ES_PROTOCOL],
host=return_dict[Constants.BEAT_ES_HOST],
port=return_dict[Constants.BEAT_ES_PORT],
username=return_dict[Constants.BEAT_HTTP_USERNAME] if (len(return_dict[Constants.BEAT_HTTP_USERNAME]) > 0) else None,
password=return_dict[Constants.BEAT_HTTP_PASSWORD] if (len(return_dict[Constants.BEAT_HTTP_PASSWORD]) > 0) else None,
ssl_verify=return_dict[Constants.BEAT_ES_SSL_VERIFY])
if (retcode == 200):
code = d.msgbox(text=Constants.MSG_TESTING_CONNECTION_SUCCESS.format("Elasticsearch", retcode, message))
else:
code = d.yesno(text=Constants.MSG_TESTING_CONNECTION_FAILURE.format("Elasticsearch", retcode, message, "\n".join(output)),
yes_label="Ignore Error", no_label="Start Over")
if code != Dialog.OK:
raise CancelledError
# test Kibana connection
if Constants.BEAT_KIBANA_HOST in return_dict.keys():
code = d.infobox(Constants.MSG_TESTING_CONNECTION.format("Kibana"))
retcode, message, output = test_connection(protocol=return_dict[Constants.BEAT_KIBANA_PROTOCOL],
host=return_dict[Constants.BEAT_KIBANA_HOST],
port=return_dict[Constants.BEAT_KIBANA_PORT],
uri="api/status",
username=return_dict[Constants.BEAT_HTTP_USERNAME] if (len(return_dict[Constants.BEAT_HTTP_USERNAME]) > 0) else None,
password=return_dict[Constants.BEAT_HTTP_PASSWORD] if (len(return_dict[Constants.BEAT_HTTP_PASSWORD]) > 0) else None,
ssl_verify=return_dict[Constants.BEAT_KIBANA_SSL_VERIFY])
if (retcode == 200):
code = d.msgbox(text=Constants.MSG_TESTING_CONNECTION_SUCCESS.format("Kibana", retcode, message))
else:
code = d.yesno(text=Constants.MSG_TESTING_CONNECTION_FAILURE.format("Kibana", retcode, message, "\n".join(output)),
yes_label="Ignore Error", no_label="Start Over")
if code != Dialog.OK:
raise CancelledError
return return_dict
###################################################################################################
###################################################################################################
def main():
locale.setlocale(locale.LC_ALL, '')
# make sure we are NOT being run as root
if os.getuid() == 0:
print(Constants.MSG_ERR_PLEBE_REQUIRED)
sys.exit(1)
# what are we (sensor vs. aggregator)
installation = Constants.DEV_UNKNOWN
modeChoices = []
try:
with open(Constants.DEV_IDENTIFIER_FILE, 'r') as f:
installation = f.readline().strip()
except:
pass
if (installation not in Constants.DEV_VALID):
print(Constants.MSG_ERR_DEV_INVALID)
sys.exit(1)
elif (installation == Constants.DEV_SENSOR):
modeChoices = [(Constants.MSG_CONFIG_MODE_CAPTURE, ""), (Constants.MSG_CONFIG_MODE_FORWARD, ""), (Constants.MSG_CONFIG_MODE_AUTOSTART, "")]
else:
print(Constants.MSG_ERR_DEV_INCORRECT.format(installation))
sys.exit(1)
start_dir = os.getcwd()
quit_flag = False
# store previously-entered elasticsearch values in case they are going through the loop
# mulitple times to prevent them from having to enter them over and over
previous_config_values = defaultdict(str)
while not quit_flag:
try:
os.chdir(start_dir)
if not os.path.isfile(Constants.SENSOR_CAPTURE_CONFIG):
# SENSOR_CAPTURE_CONFIG file doesn't exist, can't continue
raise Exception(Constants.MSG_ERROR_MISSING_CAP_CONFIG)
# read existing configuration from SENSOR_CAPTURE_CONFIG into a dictionary file (not written back out as such, just used
# as a basis for default values)
capture_config_dict = defaultdict(str)
with open(Constants.SENSOR_CAPTURE_CONFIG) as file:
for line in file:
if len(line.strip()) > 0:
name, var = remove_prefix(line, "export").partition("=")[::2]
capture_config_dict[name.strip()] = var.strip().strip("'").strip('"')
if (Constants.BEAT_ES_HOST not in previous_config_values.keys()) and ("ES_HOST" in capture_config_dict.keys()):
previous_config_values[Constants.BEAT_ES_HOST] = capture_config_dict["ES_HOST"]
previous_config_values[Constants.BEAT_KIBANA_HOST] = capture_config_dict["ES_HOST"]
if (Constants.BEAT_ES_PORT not in previous_config_values.keys()) and ("ES_PORT" in capture_config_dict.keys()):
previous_config_values[Constants.BEAT_ES_PORT] = capture_config_dict["ES_PORT"]
if (Constants.BEAT_HTTP_USERNAME not in previous_config_values.keys()) and ("ES_USERNAME" in capture_config_dict.keys()):
previous_config_values[Constants.BEAT_HTTP_USERNAME] = capture_config_dict["ES_USERNAME"]
if (Constants.ARKIME_PACKET_ACL not in previous_config_values.keys()) and ("ARKIME_PACKET_ACL" in capture_config_dict.keys()):
previous_config_values[Constants.ARKIME_PACKET_ACL] = capture_config_dict[Constants.ARKIME_PACKET_ACL]
code = d.yesno(Constants.MSG_WELCOME_TITLE, yes_label="Continue", no_label="Quit")
if (code == Dialog.CANCEL or code == Dialog.ESC):
quit_flag = True
raise CancelledError
code, mode = d.menu(Constants.MSG_CONFIG_MODE, choices=modeChoices)
if code != Dialog.OK:
quit_flag = True
raise CancelledError
if mode == Constants.MSG_CONFIG_MODE_AUTOSTART:
##### sensor autostart services configuration #######################################################################################
while True:
# select processes for autostart (except for the file scan ones, handle those with the file scanning stuff)
autostart_choices = []
for k, v in sorted(capture_config_dict.items()):
if k.startswith("AUTOSTART_"):
autostart_choices.append((k, '', v.lower() == "true"))
code, autostart_tags = d.checklist(Constants.MSG_CONFIG_AUTOSTARTS, choices=autostart_choices)
if (code == Dialog.CANCEL or code == Dialog.ESC):
raise CancelledError
for tag in [x[0] for x in autostart_choices]:
capture_config_dict[tag] = "false"
for tag in autostart_tags:
capture_config_dict[tag] = "true"
# warn them if we're doing mulitple PCAP capture processes
pcap_procs_enabled = [x for x in autostart_tags if x in Constants.PCAP_CAPTURE_AUTOSTART_ENTRIES]
if ((len(pcap_procs_enabled) <= 1) or
(d.yesno(text=Constants.MSG_WARNING_MULTIPLE_PCAP.format(", ".join(pcap_procs_enabled)),
yes_label="Continue Anyway", no_label="Adjust Selections") == Dialog.OK)):
break
# get confirmation from user that we really want to do this
code = d.yesno(Constants.MSG_CONFIG_AUTOSTART_CONFIRM.format("\n".join(sorted([f"{k}={v}" for k, v in capture_config_dict.items() if "AUTOSTART" in k]))),
yes_label="OK", no_label="Cancel")
if code == Dialog.OK:
# modify specified values in-place in SENSOR_CAPTURE_CONFIG file
autostart_re = re.compile(r"(\bAUTOSTART_\w+)\s*=\s*.+?$")
with fileinput.FileInput(Constants.SENSOR_CAPTURE_CONFIG, inplace=True, backup='.bak') as file:
for line in file:
line = line.rstrip("\n")
autostart_match = autostart_re.search(line)
if autostart_match is not None:
print(autostart_re.sub(r"\1=%s" % capture_config_dict[autostart_match.group(1)], line))
else:
print(line)
# hooray
code = d.msgbox(text=Constants.MSG_CONFIG_AUTOSTART_SUCCESS)
elif mode == Constants.MSG_CONFIG_MODE_CAPTURE:
##### sensor capture configuration ##################################################################################################
# determine a list of available (non-virtual) adapters
available_adapters = get_available_adapters()
# previously used capture interfaces
preselected_ifaces = set([x.strip() for x in capture_config_dict["CAPTURE_INTERFACE"].split(',')])
while (len(available_adapters) > 0) and (d.yesno(Constants.MSG_IDENTIFY_NICS) == Dialog.OK):
code, blinky_iface = d.radiolist(Constants.MSG_SELECT_BLINK_INTERFACE, choices=[(adapter.name, adapter.description, False) for adapter in available_adapters])
if (code == Dialog.OK) and (len(blinky_iface) > 0):
if (d.yesno(Constants.MSG_BLINK_INTERFACE.format(blinky_iface, NIC_BLINK_SECONDS), yes_label="Ready", no_label="Cancel") == Dialog.OK):
identify_adapter(adapter=blinky_iface, duration=NIC_BLINK_SECONDS, background=True)
code = d.pause(f"Identifying {blinky_iface}", seconds=NIC_BLINK_SECONDS, width=60, height=15)
elif (code != Dialog.OK):
break
# user selects interface(s) for capture
code, tag = d.checklist(Constants.MSG_SELECT_INTERFACE, choices=[(adapter.name, adapter.description, adapter.name in preselected_ifaces) for adapter in available_adapters])
if code != Dialog.OK:
raise CancelledError
selected_ifaces = tag
if (len(selected_ifaces) > 0):
# user specifies capture filter (and we validate it with tcpdump)
prev_capture_filter = capture_config_dict["CAPTURE_FILTER"]
while True:
code, capture_filter = d.inputbox("PCAP capture filter (tcpdump-like filter expression; leave blank to capture all traffic)", init=prev_capture_filter)
if (code == Dialog.CANCEL) or (code == Dialog.ESC):
raise CancelledError
capture_filter = capture_filter.strip()
if (len(capture_filter) > 0):
# test out the capture filter to see if there's a syntax error
ecode, filter_test_results = run_process(f'tcpdump -i {selected_ifaces[0]} -d "{capture_filter}"', stdout=False, stderr=True)
else:
# nothing to validate
ecode = 0
filter_test_results = [""]
if (prev_capture_filter == capture_filter) or ((ecode == 0) and
(not any(x.lower().startswith("tcpdump: warning") for x in filter_test_results)) and
(not any(x.lower().startswith("tcpdump: error") for x in filter_test_results)) and
(not any("syntax error" in x.lower() for x in filter_test_results))):
break
else:
code = d.msgbox(text=Constants.MSG_ERROR_FILTER_VALIDATION.format(" ".join([x.strip() for x in filter_test_results])))
prev_capture_filter = capture_filter
# regular expressions for selected name=value pairs to update in configuration file
capture_interface_re = re.compile(r"(\bCAPTURE_INTERFACE)\s*=\s*.+?$")
capture_filter_re = re.compile(r"(\bCAPTURE_FILTER)\s*=\s*.*?$")
pcap_path_re = re.compile(r"(\bPCAP_PATH)\s*=\s*.+?$")
zeek_path_re = re.compile(r"(\bZEEK_LOG_PATH)\s*=\s*.+?$")
zeek_carve_re = re.compile(r"(\bZEEK_EXTRACTOR_MODE)\s*=\s*.+?$")
zeek_file_preservation_re = re.compile(r"(\bEXTRACTED_FILE_PRESERVATION)\s*=\s*.+?$")
zeek_carve_override_re = re.compile(r"(\bZEEK_EXTRACTOR_OVERRIDE_FILE)\s*=\s*.*?$")
zeek_file_watch_re = re.compile(r"(\bZEEK_FILE_WATCH)\s*=\s*.+?$")
zeek_file_scanner_re = re.compile(r"(\bZEEK_FILE_SCAN_\w+)\s*=\s*.+?$")
# get paths for captured PCAP and Zeek files
while True:
code, path_values = d.form(Constants.MSG_CONFIG_CAP_PATHS, [
('PCAP Path', 1, 1, capture_config_dict.get("PCAP_PATH", ""), 1, 20, 30, 255),
('Zeek Log Path', 2, 1, capture_config_dict.get("ZEEK_LOG_PATH", ""), 2, 20, 30, 255),
])
path_values = [x.strip() for x in path_values]
if (code == Dialog.CANCEL or code == Dialog.ESC):
raise CancelledError
# paths must be specified, and must already exist
if ((len(path_values[0]) > 0) and os.path.isdir(path_values[0]) and
(len(path_values[1]) > 0) and os.path.isdir(path_values[1])):
break
else:
code = d.msgbox(text=Constants.MSG_ERROR_DIR_NOT_FOUND)
# configure file carving
code, zeek_carve_mode = d.radiolist(Constants.MSG_CONFIG_ZEEK_CARVING, choices=[(Constants.ZEEK_FILE_CARVING_NONE,
'Disable file carving',
(capture_config_dict["ZEEK_EXTRACTOR_MODE"] == Constants.ZEEK_FILE_CARVING_NONE)),
(Constants.ZEEK_FILE_CARVING_MAPPED,
'Carve files with recognized mime types',
((capture_config_dict["ZEEK_EXTRACTOR_MODE"] == Constants.ZEEK_FILE_CARVING_MAPPED) and (len(capture_config_dict["ZEEK_EXTRACTOR_OVERRIDE_FILE"]) == 0))),
(Constants.ZEEK_FILE_CARVING_MAPPED_MINUS_TEXT,
'Carve files with recognized mime types (except common plain text files)', False),
(Constants.ZEEK_FILE_CARVING_KNOWN,
'Carve files for which any mime type can be determined',
(capture_config_dict["ZEEK_EXTRACTOR_MODE"] == Constants.ZEEK_FILE_CARVING_KNOWN)),
(Constants.ZEEK_FILE_CARVING_INTERESTING,
'Carve files with mime types of common attack vectors', False),
(Constants.ZEEK_FILE_CARVING_CUSTOM_MIME,
'Use a custom selection of mime types (sorted by mime type)',
((capture_config_dict["ZEEK_EXTRACTOR_MODE"] == Constants.ZEEK_FILE_CARVING_MAPPED) and (len(capture_config_dict["ZEEK_EXTRACTOR_OVERRIDE_FILE"]) > 0))),
(Constants.ZEEK_FILE_CARVING_CUSTOM_EXT,
'Use a custom selection of mime types (sorted by file extension)', False),
(Constants.ZEEK_FILE_CARVING_ALL,
'Carve all files',
(capture_config_dict["ZEEK_EXTRACTOR_MODE"] == Constants.ZEEK_FILE_CARVING_ALL))])
if (code == Dialog.CANCEL or code == Dialog.ESC):
raise CancelledError
mime_tags = []
capture_config_dict["ZEEK_EXTRACTOR_OVERRIDE_FILE"] = ""
zeek_carved_file_preservation = PRESERVE_NONE
if zeek_carve_mode.startswith(Constants.ZEEK_FILE_CARVING_CUSTOM) or zeek_carve_mode.startswith(Constants.ZEEK_FILE_CARVING_MAPPED_MINUS_TEXT):
# get all known mime-to-extension mappings into a dictionary
all_mime_maps = mime_to_extension_mappings(Constants.ZEEK_FILE_CARVING_DEFAULTS)
if (zeek_carve_mode == Constants.ZEEK_FILE_CARVING_MAPPED_MINUS_TEXT):
# all mime types minus common text mime types
mime_tags.extend([mime for mime in all_mime_maps.keys() if mime not in Constants.ZEEK_FILE_CARVING_PLAIN_TEXT_MIMES])
else:
# select mimes to carve (pre-selecting items previously in the override file)
if (zeek_carve_mode == Constants.ZEEK_FILE_CARVING_CUSTOM_EXT):
mime_choices = [(pair[0], pair[1], pair[0] in mime_to_extension_mappings(Constants.ZEEK_FILE_CARVING_OVERRIDE_FILE)) for pair in sorted(all_mime_maps.items(), key=lambda x: x[1].lower())]
else:
mime_choices = [(pair[0], pair[1], pair[0] in mime_to_extension_mappings(Constants.ZEEK_FILE_CARVING_OVERRIDE_FILE)) for pair in sorted(all_mime_maps.items(), key=lambda x: x[0].lower())]
code, mime_tags = d.checklist(Constants.MSG_CONFIG_ZEEK_CARVING_MIMES, choices=mime_choices)
if (code == Dialog.CANCEL or code == Dialog.ESC):
raise CancelledError
mime_tags.sort()
if (len(mime_tags) == 0):
zeek_carve_mode = Constants.ZEEK_FILE_CARVING_NONE
elif (len(mime_tags) >= len(all_mime_maps)):
zeek_carve_mode = Constants.ZEEK_FILE_CARVING_MAPPED
elif len(mime_tags) > 0:
zeek_carve_mode = Constants.ZEEK_FILE_CARVING_MAPPED
capture_config_dict["ZEEK_EXTRACTOR_OVERRIDE_FILE"] = Constants.ZEEK_FILE_CARVING_OVERRIDE_FILE
else:
zeek_carve_mode = Constants.ZEEK_FILE_CARVING_MAPPED
elif zeek_carve_mode.startswith(Constants.ZEEK_FILE_CARVING_INTERESTING):
shutil.copy(Constants.ZEEK_FILE_CARVING_OVERRIDE_INTERESTING_FILE, Constants.ZEEK_FILE_CARVING_OVERRIDE_FILE)
zeek_carve_mode = Constants.ZEEK_FILE_CARVING_MAPPED
capture_config_dict["ZEEK_EXTRACTOR_OVERRIDE_FILE"] = Constants.ZEEK_FILE_CARVING_OVERRIDE_FILE
# what to do with carved files
if (zeek_carve_mode != Constants.ZEEK_FILE_CARVING_NONE):
# select engines for file scanning
scanner_choices = []
for k, v in sorted(capture_config_dict.items()):
if k.startswith("ZEEK_FILE_SCAN_"):
scanner_choices.append((k, '', v.lower() == "true"))
code, scanner_tags = d.checklist(Constants.MSG_CONFIG_ZEEK_CARVED_SCANNERS, choices=scanner_choices)
if (code == Dialog.CANCEL or code == Dialog.ESC):
raise CancelledError
for tag in [x[0] for x in scanner_choices]:
capture_config_dict[tag] = "false"
for tag in scanner_tags:
capture_config_dict[tag] = "true"
capture_config_dict["ZEEK_FILE_WATCH"] = "true" if (len(scanner_tags) > 0) else "false"
# specify what to do with files that triggered the scanner engine(s)
code, zeek_carved_file_preservation = d.radiolist(Constants.MSG_CONFIG_CARVED_FILE_PRESERVATION,
choices=[(PRESERVE_QUARANTINED,
'Preserve only quarantined files',
(capture_config_dict["EXTRACTED_FILE_PRESERVATION"] == PRESERVE_QUARANTINED)),
(PRESERVE_ALL,
'Preserve all files',
(capture_config_dict["EXTRACTED_FILE_PRESERVATION"] == PRESERVE_ALL)),
(PRESERVE_NONE,
'Preserve no files',
(capture_config_dict["EXTRACTED_FILE_PRESERVATION"] == PRESERVE_NONE))])
if (code == Dialog.CANCEL or code == Dialog.ESC):
raise CancelledError
else:
# file carving disabled, so disable file scanning as well
for key in ["ZEEK_FILE_WATCH", "ZEEK_FILE_SCAN_CLAMAV", "ZEEK_FILE_SCAN_VTOT", "ZEEK_FILE_SCAN_MALASS", "ZEEK_FILE_SCAN_YARA"]:
capture_config_dict[key] = "false"
# reconstitute dictionary with user-specified values
capture_config_dict["CAPTURE_INTERFACE"] = ",".join(selected_ifaces)
capture_config_dict["CAPTURE_FILTER"] = capture_filter
capture_config_dict["PCAP_PATH"] = path_values[0]
capture_config_dict["ZEEK_LOG_PATH"] = path_values[1]
capture_config_dict["ZEEK_EXTRACTOR_MODE"] = zeek_carve_mode
capture_config_dict["EXTRACTED_FILE_PRESERVATION"] = zeek_carved_file_preservation
# get confirmation from user that we really want to do this
code = d.yesno(Constants.MSG_CONFIG_CAP_CONFIRM.format("\n".join(sorted([f"{k}={v}" for k, v in capture_config_dict.items() if (not k.startswith("#")) and ("AUTOSTART" not in k) and ("PASSWORD" not in k)]))),
yes_label="OK", no_label="Cancel")
if code == Dialog.OK:
# modify specified values in-place in SENSOR_CAPTURE_CONFIG file
with fileinput.FileInput(Constants.SENSOR_CAPTURE_CONFIG, inplace=True, backup='.bak') as file:
for line in file:
line = line.rstrip("\n")
if capture_interface_re.search(line) is not None:
print(capture_interface_re.sub(r"\1=%s" % ",".join(selected_ifaces), line))
elif zeek_carve_override_re.search(line) is not None:
print(zeek_carve_override_re.sub(r'\1="%s"' % capture_config_dict["ZEEK_EXTRACTOR_OVERRIDE_FILE"], line))
elif zeek_carve_re.search(line) is not None:
print(zeek_carve_re.sub(r"\1=%s" % zeek_carve_mode, line))
elif zeek_file_preservation_re.search(line) is not None:
print(zeek_file_preservation_re.sub(r"\1=%s" % zeek_carved_file_preservation, line))
elif capture_filter_re.search(line) is not None:
print(capture_filter_re.sub(r'\1="%s"' % capture_filter, line))
elif pcap_path_re.search(line) is not None:
print(pcap_path_re.sub(r'\1="%s"' % capture_config_dict["PCAP_PATH"], line))
elif zeek_path_re.search(line) is not None:
print(zeek_path_re.sub(r'\1="%s"' % capture_config_dict["ZEEK_LOG_PATH"], line))
elif zeek_file_watch_re.search(line) is not None:
print(zeek_file_watch_re.sub(r"\1=%s" % capture_config_dict["ZEEK_FILE_WATCH"], line))
else:
zeek_file_scanner_match = zeek_file_scanner_re.search(line)
if zeek_file_scanner_match is not None:
print(zeek_file_scanner_re.sub(r"\1=%s" % capture_config_dict[zeek_file_scanner_match.group(1)], line))
else:
print(line)
# write out file carving overrides if specified
if (len(mime_tags) > 0) and (len(capture_config_dict["ZEEK_EXTRACTOR_OVERRIDE_FILE"]) > 0):
with open(capture_config_dict["ZEEK_EXTRACTOR_OVERRIDE_FILE"], "w+") as f:
f.write('#!/usr/bin/env zeek\n')
f.write('\n')
f.write('export {\n')
f.write(f' redef {Constants.ZEEK_FILE_CARVING_OVERRIDE_FILE_MAP_NAME} : table[string] of string = {{\n')
f.write(",\n".join([f' ["{m}"] = "{all_mime_maps[m]}"' for m in mime_tags]))
f.write('\n } &default="bin";\n')
f.write('}\n')
# hooray
code = d.msgbox(text=Constants.MSG_CONFIG_CAPTURE_SUCCESS.format(",".join(selected_ifaces), Constants.SENSOR_CAPTURE_CONFIG))
elif mode == Constants.MSG_CONFIG_MODE_FORWARD:
##### sensor forwarding (beats) configuration #########################################################################
code, fwd_mode = d.menu(Constants.MSG_CONFIG_MODE, choices=[Constants.MSG_CONFIG_FILEBEAT, Constants.MSG_CONFIG_ARKIME, Constants.MSG_CONFIG_METRICBEAT, Constants.MSG_CONFIG_AUDITBEAT, Constants.MSG_CONFIG_SYSLOGBEAT, Constants.MSG_CONFIG_HEATBEAT])
if code != Dialog.OK:
raise CancelledError
if (fwd_mode == Constants.ARKIMECAP):
# forwarding configuration for moloch-capture
# get elasticsearch/kibana connection information from user
elastic_config_dict = input_elasticsearch_connection_info(forwarder=fwd_mode,
default_es_host=previous_config_values[Constants.BEAT_ES_HOST],
default_es_port=previous_config_values[Constants.BEAT_ES_PORT],
default_username=previous_config_values[Constants.BEAT_HTTP_USERNAME],
default_password=previous_config_values[Constants.BEAT_HTTP_PASSWORD])
moloch_elastic_config_dict = elastic_config_dict.copy()
# massage the data a bit for how moloch's going to want it in the control_vars.conf file
if Constants.BEAT_HTTP_USERNAME in moloch_elastic_config_dict.keys():
moloch_elastic_config_dict["ES_USERNAME"] = moloch_elastic_config_dict.pop(Constants.BEAT_HTTP_USERNAME)
if Constants.BEAT_HTTP_PASSWORD in moloch_elastic_config_dict.keys():
moloch_elastic_config_dict["ES_PASSWORD"] = aggressive_url_encode(moloch_elastic_config_dict.pop(Constants.BEAT_HTTP_PASSWORD))
moloch_elastic_config_dict = { k.replace('BEAT_', ''): v for k, v in moloch_elastic_config_dict.items() }
# get list of IP addresses allowed for packet payload retrieval
lines = previous_config_values[Constants.ARKIME_PACKET_ACL].split(",")
lines.append(elastic_config_dict[Constants.BEAT_ES_HOST])
code, lines = d.editbox_str("\n".join(list(filter(None, list(set(lines))))), title=Constants.MSG_CONFIG_ARKIME_PCAP_ACL)
if code != Dialog.OK:
raise CancelledError
moloch_elastic_config_dict[Constants.ARKIME_PACKET_ACL] = ','.join([ip for ip in list(set(filter(None, [x.strip() for x in lines.split('\n')]))) if isipaddress(ip)])
list_results = sorted([f"{k}={v}" for k, v in moloch_elastic_config_dict.items() if ("PASSWORD" not in k) and (not k.startswith("#"))])
code = d.yesno(Constants.MSG_CONFIG_FORWARDING_CONFIRM.format(fwd_mode, "\n".join(list_results)),
yes_label="OK", no_label="Cancel")
if code != Dialog.OK:
raise CancelledError
previous_config_values = elastic_config_dict.copy()
# modify specified values in-place in SENSOR_CAPTURE_CONFIG file
elastic_values_re = re.compile(r"\b(" + '|'.join(list(moloch_elastic_config_dict.keys())) + r")\s*=\s*.*?$")
with fileinput.FileInput(Constants.SENSOR_CAPTURE_CONFIG, inplace=True, backup='.bak') as file:
for line in file:
line = line.rstrip("\n")
elastic_key_match = elastic_values_re.search(line)
if elastic_key_match is not None:
print(elastic_values_re.sub(r"\1=%s" % moloch_elastic_config_dict[elastic_key_match.group(1)], line))
else:
print(line)
# hooray
code = d.msgbox(text=Constants.MSG_CONFIG_FORWARDING_SUCCESS.format(fwd_mode, "\n".join(list_results)))
elif (fwd_mode == Constants.FILEBEAT) or (fwd_mode == Constants.METRICBEAT) or (fwd_mode == Constants.AUDITBEAT) or (fwd_mode == Constants.SYSLOGBEAT) or (fwd_mode == Constants.HEATBEAT):
# forwarder configuration for beats
if not os.path.isdir(Constants.BEAT_DIR[fwd_mode]):
# beat dir not found, give up
raise Exception(Constants.MSG_ERROR_FWD_DIR_NOT_FOUND.format(Constants.BEAT_DIR[fwd_mode], fwd_mode))
# chdir to the beat directory
os.chdir(Constants.BEAT_DIR[fwd_mode])
# check to see if a keystore has already been created for the forwarder
ecode, list_results = run_process(f"{Constants.BEAT_CMD[fwd_mode]} keystore list")
if (ecode == 0) and (len(list_results) > 0):
# it has, do they wish to overwrite it?
if (d.yesno(Constants.MSG_OVERWRITE_CONFIG.format(fwd_mode)) != Dialog.OK):
raise CancelledError
ecode, create_results = run_process(f"{Constants.BEAT_CMD[fwd_mode]} keystore create --force", stderr=True)
if (ecode != 0):
# keystore creation failed
raise Exception(Constants.MSG_ERROR_KEYSTORE.format(fwd_mode, "\n".join(create_results)))
forwarder_dict = defaultdict(str)
if (fwd_mode == Constants.METRICBEAT) or (fwd_mode == Constants.AUDITBEAT) or (fwd_mode == Constants.SYSLOGBEAT) or (fwd_mode == Constants.HEATBEAT):
#### auditbeat/metricbeat/filebeat-syslog ###################################################################
# enter beat configuration (in a few steps)
if (fwd_mode == Constants.METRICBEAT):
# interval is metricbeat only, the rest is used by both
code, beat_interval = d.rangebox(f"{Constants.MSG_CONFIG_GENERIC.format(fwd_mode)} interval (seconds)",
width=60, min=1, max=60, init=30)
if (code == Dialog.CANCEL or code == Dialog.ESC):
raise CancelledError
forwarder_dict[Constants.BEAT_INTERVAL] = f"{beat_interval}s"
# get elasticsearch/kibana connection information from user
forwarder_dict.update(input_elasticsearch_connection_info(forwarder=fwd_mode,
default_es_host=previous_config_values[Constants.BEAT_ES_HOST],
default_es_port=previous_config_values[Constants.BEAT_ES_PORT],
default_kibana_host=previous_config_values[Constants.BEAT_KIBANA_HOST],
default_kibana_port=previous_config_values[Constants.BEAT_KIBANA_PORT],
default_username=previous_config_values[Constants.BEAT_HTTP_USERNAME],
default_password=previous_config_values[Constants.BEAT_HTTP_PASSWORD]))
elif (fwd_mode == Constants.FILEBEAT):
#### filebeat #############################################################################################
while True:
forwarder_dict = defaultdict(str)
# enter main filebeat configuration
code, values = d.form(Constants.MSG_CONFIG_GENERIC.format(fwd_mode), [
('Log Path', 1, 1, capture_config_dict["ZEEK_LOG_PATH"], 1, 20, 30, 255),
('Destination Host', 2, 1, "", 2, 20, 30, 255),
('Destination Port', 3, 1, "5044", 3, 20, 6, 5)
])
values = [x.strip() for x in values]
if (code == Dialog.CANCEL) or (code == Dialog.ESC):
raise CancelledError
elif (len(values[0]) <= 0) or (not os.path.isdir(values[0])):
code = d.msgbox(text=Constants.MSG_ERROR_DIR_NOT_FOUND)
elif (len(values[1]) <= 0) or (len(values[2]) <= 0) or (not values[2].isnumeric()):
code = d.msgbox(text=Constants.MSG_ERROR_BAD_HOST)
else:
forwarder_dict[Constants.BEAT_LOG_PATTERN_KEY] = os.path.join(os.path.join(values[0], Constants.BEAT_LOG_PATH_SUBDIR), Constants.BEAT_LOG_PATTERN_VAL)
forwarder_dict[Constants.BEAT_LS_HOST] = values[1]
forwarder_dict[Constants.BEAT_LS_PORT] = values[2]
break
# optionally, filebeat can use SSL if Logstash is configured for it
logstash_ssl = "false"
logstash_ssl_verify = "none"
if (d.yesno("Forward Zeek logs over SSL? (Note: This requires the destination to be similarly configured and a corresponding copy of the client SSL files.)", yes_label="SSL", no_label="Unencrypted") == Dialog.OK):
logstash_ssl = "true"
if (d.yesno("Logstash SSL verification", yes_label="None", no_label="Force Peer") != Dialog.OK):
logstash_ssl_verify = "force_peer"
forwarder_dict[Constants.BEAT_LS_SSL] = logstash_ssl
forwarder_dict[Constants.BEAT_LS_SSL_VERIFY] = logstash_ssl_verify
if (forwarder_dict[Constants.BEAT_LS_SSL] == "true"):
while True:
code, values = d.form(Constants.MSG_CONFIG_GENERIC.format(fwd_mode), [
('SSL Certificate Authorities File', 1, 1, "", 1, 35, 30, 255),
('SSL Certificate File', 2, 1, "", 2, 35, 30, 255),
('SSL Key File', 3, 1, "", 3, 35, 30, 255),
])
values = [x.strip() for x in values]
if (code == Dialog.CANCEL) or (code == Dialog.ESC):
raise CancelledError
elif ((len(values[0]) <= 0) or (not os.path.isfile(values[0])) or
(len(values[1]) <= 0) or (not os.path.isfile(values[1])) or
(len(values[2]) <= 0) or (not os.path.isfile(values[2]))):
code = d.msgbox(text=Constants.MSG_ERROR_FILE_NOT_FOUND)
else:
forwarder_dict[Constants.BEAT_LS_SSL_CA_CRT] = values[0]
forwarder_dict[Constants.BEAT_LS_SSL_CLIENT_CRT] = values[1]
forwarder_dict[Constants.BEAT_LS_SSL_CLIENT_KEY] = values[2]
break
else:
forwarder_dict[Constants.BEAT_LS_SSL_CA_CRT] = ""
forwarder_dict[Constants.BEAT_LS_SSL_CLIENT_CRT] = ""
forwarder_dict[Constants.BEAT_LS_SSL_CLIENT_KEY] = ""
# see if logstash port is open (not a great connection test, but better than nothing!)
code = d.infobox(Constants.MSG_TESTING_CONNECTION.format("Logstash"))
if not check_socket(forwarder_dict[Constants.BEAT_LS_HOST], int(forwarder_dict[Constants.BEAT_LS_PORT])):
code = d.yesno(text=Constants.MSG_TESTING_CONNECTION_FAILURE_LOGSTASH.format("Logstash", forwarder_dict[Constants.BEAT_LS_HOST], forwarder_dict[Constants.BEAT_LS_PORT]),
yes_label="Ignore Error", no_label="Start Over")
if code != Dialog.OK:
raise CancelledError
# outside of filebeat/metricbeat if/else, get confirmation and write out the values to the keystore
if forwarder_dict:
# get confirmation of parameters before we pull the trigger
code = d.yesno(Constants.MSG_CONFIG_FORWARDING_CONFIRM.format(fwd_mode, "\n".join(sorted([f"{k}={v}" for k, v in forwarder_dict.items() if "PASSWORD" not in k]))),
yes_label="OK", no_label="Cancel")
if code != Dialog.OK:
raise CancelledError
previous_config_values = forwarder_dict.copy()
# it's go time, call keystore add for each item
for k, v in sorted(forwarder_dict.items()):
ecode, add_results = run_process(f"{Constants.BEAT_CMD[fwd_mode]} keystore add {k} --stdin --force", stdin=v, stderr=True)
if (ecode != 0):
# keystore creation failed
raise Exception(Constants.MSG_ERROR_KEYSTORE.format(fwd_mode, "\n".join(add_results)))
# get a final list of parameters that were set to show the user that stuff happened
ecode, list_results = run_process(f"{Constants.BEAT_CMD[fwd_mode]} keystore list")
if (ecode == 0):
code = d.msgbox(text=Constants.MSG_CONFIG_FORWARDING_SUCCESS.format(fwd_mode, "\n".join(list_results)))
else:
# keystore list failed
raise Exception(Constants.MSG_ERROR_KEYSTORE.format(fwd_mode, "\n".join(add_results)))
else:
# we got through the config but ended up with no values for configuration!
raise Exception(Constants.MSG_MESSAGE_ERROR.format(Constants.MSG_EMPTY_CONFIG_ERROR))
except CancelledError as c:
# d.msgbox(text=Constants.MSG_CANCEL_ERROR)
# just start over
continue
except Exception as e:
d.msgbox(text=Constants.MSG_MESSAGE_ERROR.format(e))
raise
if __name__ == '__main__':
main()
clearquit()