initial commit
This commit is contained in:
104
.gitignore
vendored
Normal file
104
.gitignore
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
.static_storage/
|
||||
.media/
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
26
README
Normal file
26
README
Normal file
@@ -0,0 +1,26 @@
|
||||
Configure Kafka topics (run on one kafka node)
|
||||
doc/kafka_topics.sh
|
||||
|
||||
Initialize elasticsearch:
|
||||
curl -X PUT 'http://<elasticsearch>:9200/threatline' -d@doc/es_mapping.json
|
||||
|
||||
Install service file (FreeBSD):
|
||||
cp doc/threatline /usr/local/etc/rc.d/threatline
|
||||
|
||||
Enable threatline:
|
||||
sysrc threatline_enable=YES
|
||||
sysrc threatline_agents="normalize enrich check archive"
|
||||
|
||||
Start threatline:
|
||||
service threatline start
|
||||
|
||||
Monitor logs:
|
||||
tail -f /tmp/tl_worker.log
|
||||
|
||||
|
||||
Stages:
|
||||
Normalize: Touch-up/rename fields, etc.
|
||||
Enrich: Enrich and part of the message.
|
||||
Check: Checks parts of message (now enriched) against known bad stuff.
|
||||
Archive: Push document into elasticsearch. Can also log to file.
|
||||
|
||||
BIN
doc/APACHE_KAFKA.tgz
Normal file
BIN
doc/APACHE_KAFKA.tgz
Normal file
Binary file not shown.
2117
doc/es_mapping.json
Normal file
2117
doc/es_mapping.json
Normal file
File diff suppressed because it is too large
Load Diff
11
doc/kafka_topics.sh
Normal file
11
doc/kafka_topics.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
KBIN=/usr/local/share/java/kafka/bin
|
||||
${KBIN}/kafka-topics.sh --zookeeper 10.15.0.38:2181 --create --topic TLINGEST-lodi --partitions=3 --replication-factor=1
|
||||
${KBIN}/kafka-topics.sh --zookeeper 10.15.0.38:2181 --create --topic TLINGEST-wyomic --partitions=3 --replication-factor=1
|
||||
${KBIN}/kafka-topics.sh --zookeeper 10.15.0.38:2181 --create --topic TLINGEST-qwf --partitions=3 --replication-factor=1
|
||||
${KBIN}/kafka-topics.sh --zookeeper 10.15.0.38:2181 --create --topic TLNORMALIZED --partitions=3 --replication-factor=1
|
||||
${KBIN}/kafka-topics.sh --zookeeper 10.15.0.38:2181 --create --topic TLENRICHED --partitions=3 --replication-factor=1
|
||||
${KBIN}/kafka-topics.sh --zookeeper 10.15.0.38:2181 --create --topic TLALERTS --partitions=3 --replication-factor=1
|
||||
${KBIN}/kafka-topics.sh --zookeeper 10.15.0.38:2181 --create --topic TLARCHIVE --partitions=3 --replication-factor=1
|
||||
|
||||
101
doc/setup_pfsense.sh
Normal file
101
doc/setup_pfsense.sh
Normal file
@@ -0,0 +1,101 @@
|
||||
# FreeBSD 11.1-RELEASE
|
||||
|
||||
# Install dependencies
|
||||
pkg install -y bash git flex bison cmake libpcap librdkafka python py27-sqlite3 caf swig
|
||||
|
||||
# Compile Bro (no install)
|
||||
# Needs compiled because build/src/bifcl is needed to compile plugins
|
||||
mkdir /usr/local/src; cd /usr/local/src/
|
||||
git clone https://github.com/bro/bro
|
||||
cd bro; ./configure && make -j2
|
||||
|
||||
# Compile kafka plugin (no install)
|
||||
# This will generate APACHE_KAFKA.tar.gz
|
||||
cd /usr/local/src/
|
||||
git clone https://github.com/apache/metron-bro-plugin-kafka.git
|
||||
./configure --bro-dist=/usr/local/src/bro
|
||||
make
|
||||
|
||||
# Copy APACHE_KAFKA.tgz to pfsense
|
||||
# Login into pfsense and enable FreeBSD repos (temporarily)
|
||||
sed -i '' 's/FreeBSD: { enabled: no/FreeBSD: { enabled: yes/g' /usr/local/share/pfSense/pkg/repos/pfSense-repo.conf
|
||||
sed -i '' 's/FreeBSD: { enabled: no/FreeBSD: { enabled: yes/g' /usr/local/etc/pkg/repos/FreeBSD.conf
|
||||
pkg install -y bro librdkafka
|
||||
sed -i '' 's/FreeBSD: { enabled: yes/FreeBSD: { enabled: no/g' /usr/local/share/pfSense/pkg/repos/pfSense-repo.conf
|
||||
sed -i '' 's/FreeBSD: { enabled: yes/FreeBSD: { enabled: no/g' /usr/local/etc/pkg/repos/FreeBSD.conf
|
||||
pkg update
|
||||
|
||||
# Extract plugin and enable it
|
||||
tar xzf APACHE_KAFKA.tgz -C /usr/local/lib/bro/plugins
|
||||
cat > /usr/local/share/bro/site/local.bro <<EOF
|
||||
@load misc/loaded-scripts
|
||||
@load tuning/defaults
|
||||
@load misc/capture-loss
|
||||
@load misc/stats
|
||||
@load misc/scan
|
||||
@load frameworks/software/vulnerable
|
||||
@load frameworks/software/version-changes
|
||||
@load-sigs frameworks/signatures/detect-windows-shells
|
||||
@load protocols/ftp/software
|
||||
@load protocols/smtp/software
|
||||
@load protocols/ssh/software
|
||||
@load protocols/http/software
|
||||
@load protocols/ftp/detect
|
||||
@load protocols/conn/known-hosts
|
||||
@load protocols/conn/known-services
|
||||
@load protocols/ssl/known-certs
|
||||
@load protocols/ssl/validate-certs
|
||||
@load protocols/ssl/log-hostcerts-only
|
||||
@load protocols/ssh/geo-data
|
||||
@load protocols/ssh/detect-bruteforcing
|
||||
@load protocols/ssh/interesting-hostnames
|
||||
@load frameworks/files/hash-all-files
|
||||
@load frameworks/files/detect-MHR
|
||||
@load policy/protocols/conn/vlan-logging
|
||||
@load policy/protocols/conn/mac-logging
|
||||
@load policy/protocols/smb
|
||||
@load Apache/Kafka/logs-to-kafka.bro
|
||||
redef Kafka::topic_name = "TLINGEST-CLIENTNAME";
|
||||
redef Kafka::tag_json = T;
|
||||
redef Kafka::logs_to_send = set(CaptureLoss::LOG, PacketFilter::LOG, Stats::LOG, Conn::LOG, DHCP::LOG, DNS::LOG, FTP::LOG, HTTP::LOG, IRC::LOG, KRB::LOG, NTLM::LOG, RADIUS::LOG, RDP::LOG, SIP::LOG, SMB::CMD_LOG, SMB::FILES_LOG, SMB::MAPPING_LOG, SMTP::LOG, SNMP::LOG, SOCKS::LOG, SSH::LOG, SSL::LOG, Syslog::LOG, Tunnel::LOG, Files::LOG, PE::LOG, X509::LOG, Intel::LOG, Notice::LOG, Software::LOG, Weird::LOG, CaptureLoss::LOG);
|
||||
redef Kafka::kafka_conf = table(["client.id"] = "setme.client"
|
||||
, ["compression.codec"] = "lz4"
|
||||
, ["request.required.acks"] = "0"
|
||||
, ["metadata.broker.list"] = "10.15.0.40:9092,10.15.0.41:9092,10.15.0.42:9092"
|
||||
);
|
||||
EOF
|
||||
|
||||
cat > /usr/local/etc/node.cfg <<EOF
|
||||
[logger]
|
||||
type=logger
|
||||
host=localhost
|
||||
[manager]
|
||||
type=manager
|
||||
host=localhost
|
||||
[proxy-1]
|
||||
type=proxy
|
||||
host=localhost
|
||||
[worker-1]
|
||||
type=worker
|
||||
host=localhost
|
||||
interface=igb1
|
||||
EOF
|
||||
|
||||
cat > /usr/local/etc/broctl.cfg <<EOF
|
||||
MailTo = root@localhost
|
||||
MailConnectionSummary = 0
|
||||
MinDiskSpace = 0
|
||||
MailHostUpDown = 0
|
||||
LogRotationInterval = 3600
|
||||
LogExpireInterval = 0
|
||||
StatsLogEnable = 1
|
||||
StatsLogExpireInterval = 1
|
||||
StatusCmdShowAll = 0
|
||||
CrashExpireInterval = 1
|
||||
SitePolicyScripts = local.bro
|
||||
LogDir = /usr/local/logs
|
||||
SpoolDir = /usr/local/spool
|
||||
CfgDir = /usr/local/etc
|
||||
EOF
|
||||
|
||||
broctl check
|
||||
51
doc/threatline
Normal file
51
doc/threatline
Normal file
@@ -0,0 +1,51 @@
|
||||
# PROVIDE: threatline
|
||||
# REQUIRE: LOGIN
|
||||
# KEYWORD: shutdown
|
||||
|
||||
# Add the following lines to /etc/rc.conf to enable threatline:
|
||||
#
|
||||
# threatline_enable="YES"
|
||||
#
|
||||
#
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="threatline"
|
||||
rcvar=threatline_enable
|
||||
|
||||
# read configuration and set defaults
|
||||
load_rc_config "$name"
|
||||
: ${threatline_enable="NO"}
|
||||
: ${threatline_agents="normalize enrich check archive"}
|
||||
: ${threatline_pidfile="/tmp/threatline.pid"}
|
||||
: ${threatline_path="/usr/local/threatline/threatline/threatline.py"}
|
||||
|
||||
start_cmd="threatline_start"
|
||||
stop_cmd="threatline_stop"
|
||||
daemon_head=/usr/sbin/daemon
|
||||
python_path=/usr/local/bin/python2.7
|
||||
|
||||
threatline_start()
|
||||
{
|
||||
if checkyesno ${rcvar}; then
|
||||
echo "* starting threatline... "
|
||||
$daemon_head -p $threatline_pidfile $python_path $threatline_path $threatline_agents
|
||||
fi
|
||||
}
|
||||
|
||||
threatline_stop()
|
||||
{
|
||||
if checkyesno ${rcvar}; then
|
||||
echo "* stopping threatline... "
|
||||
#pkill python
|
||||
kill `ps ax | awk '/threatline/{print $1}'` 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
threatline_restart()
|
||||
{
|
||||
threatline_stop
|
||||
threatline_start
|
||||
}
|
||||
|
||||
run_rc_command "$1"
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
elasticsearch
|
||||
kafka-python
|
||||
lz4
|
||||
ipwhois
|
||||
ipaddress
|
||||
requests
|
||||
netaddr
|
||||
11
setup.py
Normal file
11
setup.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from distutils.core import setup
|
||||
|
||||
setup(
|
||||
name='ThreatLine',
|
||||
version='0.1',
|
||||
packages=['threatline'],
|
||||
license='Creative Commons Attribution-Noncommercial-Share Alike license',
|
||||
long_description=open('README.txt').read(),
|
||||
)
|
||||
0
threatline/handlers/__init__.py
Normal file
0
threatline/handlers/__init__.py
Normal file
100
threatline/handlers/archive.py
Normal file
100
threatline/handlers/archive.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
# Archiving handler
|
||||
|
||||
import sys
|
||||
from base import BaseHandler
|
||||
from utils.archive_utils import ElasticLogger
|
||||
|
||||
|
||||
class Archive(BaseHandler):
|
||||
|
||||
def handle_conn(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_dhcp(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_dce_rpc(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_known_devices(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_dns(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_files(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_http(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_notice(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_smtp(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_snmp(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_software(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_ssh(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_ssl(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_stats(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_syslog(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_weird(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_x509(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_intel(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_capture_loss(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_communication(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_ntlm(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_pe(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_smb_files(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_smb_mapping(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_tunnel(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def handle_rdp(self, message):
|
||||
self.archiver.send(message)
|
||||
|
||||
def __init__(self):
|
||||
super(Archive, self).__init__()
|
||||
self.mymod = sys.modules[__name__].Archive
|
||||
self.settings = {
|
||||
'consumer_topic': 'TLARCHIVE',
|
||||
'consumer_group': 'archive_group',
|
||||
'producer_topic': 'TLDONE',
|
||||
'producer_client_id': 'archiver-',
|
||||
'dispatchers': {} # will be generated at initialize()
|
||||
}
|
||||
# self.archiver = FileLogger('threatline.log')
|
||||
self.archiver = ElasticLogger()
|
||||
100
threatline/handlers/base.py
Normal file
100
threatline/handlers/base.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
# Ingestion handler
|
||||
|
||||
import sys
|
||||
from functools import partial
|
||||
|
||||
|
||||
class BaseHandler(object):
|
||||
|
||||
def __init__(self):
|
||||
self.settings = {}
|
||||
self.dispatch = {}
|
||||
self.mymod = None
|
||||
|
||||
def handle_conn(self, message):
|
||||
return message
|
||||
|
||||
def handle_dce_rpc(self, message):
|
||||
return message
|
||||
|
||||
def handle_known_devices(self, message):
|
||||
return message
|
||||
|
||||
def handle_dhcp(self, message):
|
||||
return message
|
||||
|
||||
def handle_dns(self, message):
|
||||
return message
|
||||
|
||||
def handle_files(self, message):
|
||||
return message
|
||||
|
||||
def handle_http(self, message):
|
||||
return message
|
||||
|
||||
def handle_notice(self, message):
|
||||
return message
|
||||
|
||||
def handle_smtp(self, message):
|
||||
return message
|
||||
|
||||
def handle_snmp(self, message):
|
||||
return message
|
||||
|
||||
def handle_software(self, message):
|
||||
return message
|
||||
|
||||
def handle_ssh(self, message):
|
||||
return message
|
||||
|
||||
def handle_ssl(self, message):
|
||||
return message
|
||||
|
||||
def handle_stats(self, message):
|
||||
return message
|
||||
|
||||
def handle_syslog(self, message):
|
||||
return message
|
||||
|
||||
def handle_weird(self, message):
|
||||
return message
|
||||
|
||||
def handle_x509(self, message):
|
||||
return message
|
||||
|
||||
def handle_intel(self, message):
|
||||
return message
|
||||
|
||||
def handle_capture_loss(self, message):
|
||||
return message
|
||||
|
||||
def handle_communication(self, message):
|
||||
return message
|
||||
|
||||
def handle_ntlm(self, message):
|
||||
return message
|
||||
|
||||
def handle_pe(self, message):
|
||||
return message
|
||||
|
||||
def handle_smb_files(self, message):
|
||||
return message
|
||||
|
||||
def handle_smb_mapping(self, message):
|
||||
return message
|
||||
|
||||
def handle_tunnel(self, message):
|
||||
return message
|
||||
|
||||
def handle_rdp(self, message):
|
||||
return message
|
||||
|
||||
def initialize(self):
|
||||
for lm in dir(self.mymod):
|
||||
if lm.startswith('handle_'):
|
||||
name = lm.replace('handle_', '')
|
||||
# Bind each method found, to this instance (self)
|
||||
self.dispatch[name] = partial(getattr(self.mymod, lm), self)
|
||||
|
||||
self.settings['dispatchers'] = self.dispatch
|
||||
35
threatline/handlers/check.py
Normal file
35
threatline/handlers/check.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
# Check handler
|
||||
|
||||
import sys
|
||||
from base import BaseHandler
|
||||
from utils.check_utils import Checker
|
||||
|
||||
|
||||
class Check(BaseHandler):
|
||||
|
||||
def check_ioc(self, dtype, data):
|
||||
if dtype == 'domain':
|
||||
return self.checker.check_domain(data)
|
||||
else:
|
||||
return {}
|
||||
|
||||
def handle_dns(self, message):
|
||||
if len(message['query']) == 0:
|
||||
return message
|
||||
domain = message['query']
|
||||
message['alert'] = self.check_ioc('domain', domain)
|
||||
return message
|
||||
|
||||
def __init__(self):
|
||||
super(Check, self).__init__()
|
||||
self.mymod = sys.modules[__name__].Check
|
||||
self.checker = Checker()
|
||||
self.settings = {
|
||||
'consumer_topic': 'TLENRICHED',
|
||||
'consumer_group': 'check_group',
|
||||
'producer_topic': 'TLARCHIVE',
|
||||
'alert_topic': 'TLALERTS',
|
||||
'producer_client_id': 'checker-',
|
||||
'dispatchers': {} # will be generated at initialize()
|
||||
}
|
||||
41
threatline/handlers/enrich.py
Normal file
41
threatline/handlers/enrich.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python
|
||||
# Enrichment handler
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from base import BaseHandler
|
||||
from functools import partial
|
||||
from utils.enrich_utils import IPClient, mac_lookup
|
||||
|
||||
|
||||
class Enrich(BaseHandler):
|
||||
|
||||
def handle_conn(self, message):
|
||||
message['enrichment'] = {}
|
||||
if message['local_orig']: # If True, means conn originated locally
|
||||
e = self.ipcli.enrichip(message['id.resp_h'])
|
||||
else:
|
||||
e = self.ipcli.enrichip(message['id.orig_h'])
|
||||
if e:
|
||||
message['enrichment']['ip'] = e
|
||||
return message
|
||||
|
||||
def handle_known_devices(self, message):
|
||||
message['enrichment'] = {}
|
||||
ret = mac_lookup(message['mac'])
|
||||
if not ret:
|
||||
ret = {'note': 'Not Registered!'}
|
||||
message['enrichment']['mac'] = ret
|
||||
return message
|
||||
|
||||
def __init__(self):
|
||||
super(Enrich, self).__init__()
|
||||
self.ipcli = IPClient()
|
||||
self.mymod = sys.modules[__name__].Enrich
|
||||
self.settings = {
|
||||
'consumer_topic': 'TLNORMALIZED',
|
||||
'consumer_group': 'enrich_group',
|
||||
'producer_topic': 'TLENRICHED',
|
||||
'producer_client_id': 'enricher-',
|
||||
'dispatchers': {} # will be generated at initialize()
|
||||
}
|
||||
53
threatline/handlers/normalize.py
Normal file
53
threatline/handlers/normalize.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python
|
||||
# Ingestion handler
|
||||
|
||||
import sys
|
||||
from ipaddress import ip_address
|
||||
from base import BaseHandler
|
||||
|
||||
|
||||
class Normalize(BaseHandler):
|
||||
|
||||
def handle_x509(self, message):
|
||||
# for some reason the uid for x509 log types
|
||||
# are 'id' instead. Lets fix that.
|
||||
message['uid'] = message['id']
|
||||
_ = message.pop('id')
|
||||
return(message)
|
||||
|
||||
def handle_pe(self, message):
|
||||
# same issuse as x509.
|
||||
message['uid'] = message['id']
|
||||
_ = message.pop('id')
|
||||
return(message)
|
||||
|
||||
def handle_software(self, message):
|
||||
fields = ('version.major', 'version.minor', 'version.minor2',
|
||||
'version.minor3', 'version.addl')
|
||||
for old in fields:
|
||||
new = old.replace('.', '_')
|
||||
try:
|
||||
message[new] = message[old]
|
||||
_ = message.pop(old)
|
||||
except KeyError:
|
||||
continue
|
||||
return(message)
|
||||
|
||||
def handle_conn(self, message):
|
||||
q = ip_address(message['id.orig_h'])
|
||||
if q.version == 6:
|
||||
return # don't pass along IPv6
|
||||
return message
|
||||
|
||||
def __init__(self):
|
||||
super(Normalize, self).__init__()
|
||||
self.mymod = sys.modules[__name__].Normalize
|
||||
self.settings = {
|
||||
'consumer_topic': ['TLINGEST-qwf',
|
||||
'TLINGEST-wyomic',
|
||||
'TLINGEST-lodi'],
|
||||
'consumer_group': 'normalize_group',
|
||||
'producer_topic': 'TLNORMALIZED',
|
||||
'producer_client_id': 'normalizer-',
|
||||
'dispatchers': {} # will be generated at initialize()
|
||||
}
|
||||
0
threatline/handlers/utils/__init__.py
Normal file
0
threatline/handlers/utils/__init__.py
Normal file
50
threatline/handlers/utils/archive_utils.py
Normal file
50
threatline/handlers/utils/archive_utils.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import json
|
||||
from elasticsearch import Elasticsearch
|
||||
|
||||
|
||||
class FileLogger(object):
|
||||
|
||||
def __init__(self, logfile):
|
||||
self.logfile = logfile
|
||||
self.flogger = open(self.logfile, 'a')
|
||||
|
||||
def send(self, message):
|
||||
""" this buffers writes, be patient if your
|
||||
tailing the log file """
|
||||
self.flogger.write(json.dumps(message))
|
||||
self.flogger.write('\n')
|
||||
|
||||
|
||||
class ElasticLogger(object):
|
||||
|
||||
def __init__(self):
|
||||
self.es = Elasticsearch(hosts=['10.15.0.45:9200',
|
||||
'10.15.0.46:9200',
|
||||
'10.15.0.47:9200'])
|
||||
self.mapped_types = ('files', 'notice', 'http', 'reporter',
|
||||
'communication', 'packet_filter', 'ssl', 'dpd',
|
||||
'capture_loss', 'dns', 'loaded_scripts', 'stats',
|
||||
'weird', 'conn', 'x509', 'ssh', 'pe', 'smb_files',
|
||||
'smb_mapping', 'snmp', 'dce_rpc', 'smtp', 'rdp',
|
||||
'software', 'tunnel', 'known_certs',
|
||||
'known_devices', 'known_hosts', 'known_services',
|
||||
'dhcp')
|
||||
|
||||
def send(self, message):
|
||||
self.doctype = message['tltype']
|
||||
self.docindex = 'threatline' # + self.doctype
|
||||
""" Bro creates timestamps as floats like
|
||||
xxxxxxxxx.xxxxx ... don't want this. """
|
||||
message['ts'] = int(message['ts'])
|
||||
try:
|
||||
if self.doctype in self.mapped_types:
|
||||
self.es.index(index=self.docindex,
|
||||
doc_type=self.doctype,
|
||||
body=message)
|
||||
else:
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
18
threatline/handlers/utils/check_utils.py
Normal file
18
threatline/handlers/utils/check_utils.py
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import logging
|
||||
from threat_intel import domain_intel
|
||||
|
||||
|
||||
class Checker(object):
|
||||
|
||||
def __init__(self):
|
||||
self.dnsbl = domain_intel()
|
||||
|
||||
def check_domain(self, domain):
|
||||
if domain in self.dnsbl:
|
||||
return {'alert_type': 'domain',
|
||||
'domain': domain,
|
||||
'info': self.dnsbl[domain]}
|
||||
return None
|
||||
62
threatline/handlers/utils/enrich_utils.py
Normal file
62
threatline/handlers/utils/enrich_utils.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import logging
|
||||
from ipwhois import IPWhois, utils
|
||||
from ipaddress import ip_address
|
||||
from netaddr import *
|
||||
|
||||
def mac_lookup(mac_address):
|
||||
ret = {}
|
||||
mac = EUI(mac_address)
|
||||
try:
|
||||
oui = mac.oui
|
||||
except Exception as e:
|
||||
logging.error('MAC {} not registered'.format(mac))
|
||||
return None
|
||||
ret['org'] = oui.registration().org
|
||||
ret['address'] = ' '.join(oui.registration().address)
|
||||
return ret
|
||||
|
||||
|
||||
class IPClient(object):
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {} # cache for faster whois lookups
|
||||
|
||||
def is_defined(self, addr):
|
||||
""" Checks if the IP is defined as loopback, multicast, etc. """
|
||||
queryip = ip_address(addr)
|
||||
if queryip.version == 4:
|
||||
defined, _, _ = utils.ipv4_is_defined(addr)
|
||||
if queryip.version == 6:
|
||||
defined, _, _ = utils.ipv6_is_defined(addr)
|
||||
return defined
|
||||
|
||||
def enrichip(self, addr):
|
||||
""" Enriches the IP address if it's not reserved (defined) IP. """
|
||||
if addr in self.cache:
|
||||
logging.info('Whois cache hit')
|
||||
return self.cache[addr]
|
||||
|
||||
if self.is_defined(addr):
|
||||
return None
|
||||
|
||||
self.queryip = IPWhois(addr)
|
||||
try:
|
||||
r = self.queryip.lookup_rdap()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
ent = r['entities'][0]
|
||||
e = {'asn_num': r['asn'],
|
||||
'asn_desc': r['asn_description'],
|
||||
'asn_country_code': r['asn_country_code'],
|
||||
'network': r['network']['cidr'],
|
||||
'whois': r['objects'][ent]['contact']}
|
||||
|
||||
_ = e['whois'].pop('address')
|
||||
_ = e['whois'].pop('email')
|
||||
|
||||
self.cache[addr] = e
|
||||
return e
|
||||
26
threatline/handlers/utils/threat_intel.py
Normal file
26
threatline/handlers/utils/threat_intel.py
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import logging
|
||||
import requests
|
||||
import ipaddress
|
||||
|
||||
|
||||
domain_intel_sources = [
|
||||
('MALWAREDOMAIN', 'http://bld.scoutsec.com/?genres=malware&style=list')
|
||||
]
|
||||
|
||||
|
||||
def get_download(url):
|
||||
try:
|
||||
return requests.get(url)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
|
||||
def domain_intel():
|
||||
for source, url in domain_intel_sources:
|
||||
d = get_download(url).text.split('\n')
|
||||
d = {domain: source for domain in d}
|
||||
return d
|
||||
136
threatline/threatline.py
Executable file
136
threatline/threatline.py
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import signal
|
||||
import logging
|
||||
from kafka import KafkaConsumer, KafkaProducer
|
||||
from kafka.errors import KafkaError
|
||||
from multiprocessing import Process
|
||||
|
||||
VALID_STAGES = ['normalize', 'enrich', 'check', 'archive']
|
||||
KAFKA_BOOTSTRAP = ['10.15.0.40:9092', '10.15.0.41:9092', '10.15.0.42:9092']
|
||||
LOGFORMAT = '%(asctime)-15s %(message)s'
|
||||
|
||||
|
||||
def get_consumer(topic, consumer_group):
|
||||
consumer = KafkaConsumer(
|
||||
value_deserializer=lambda m: json.loads(m.decode('utf-8')),
|
||||
bootstrap_servers=KAFKA_BOOTSTRAP,
|
||||
group_id=consumer_group,
|
||||
enable_auto_commit=True
|
||||
)
|
||||
consumer.subscribe(topic)
|
||||
return consumer
|
||||
|
||||
|
||||
def get_producer(client_id):
|
||||
producer = KafkaProducer(
|
||||
value_serializer=lambda m: json.dumps(m).encode('utf-8'),
|
||||
bootstrap_servers=KAFKA_BOOTSTRAP,
|
||||
client_id=client_id,
|
||||
compression_type='lz4'
|
||||
)
|
||||
return producer
|
||||
|
||||
|
||||
def main(stage):
|
||||
LOGFILE = '/var/log/' + stage + '_worker.log'
|
||||
logging.basicConfig(level=logging.INFO, format=LOGFORMAT,
|
||||
filename=LOGFILE)
|
||||
logging.info('Launching {}'.format(stage))
|
||||
logging.info('Singals registered')
|
||||
|
||||
normalize_flag = False
|
||||
archive_flag = False
|
||||
if stage == 'normalize':
|
||||
from handlers.normalize import Normalize
|
||||
handle = Normalize()
|
||||
normalize_flag = True
|
||||
elif stage == 'enrich':
|
||||
from handlers.enrich import Enrich
|
||||
handle = Enrich()
|
||||
elif stage == 'check':
|
||||
from handlers.check import Check
|
||||
handle = Check()
|
||||
elif stage == 'archive':
|
||||
from handlers.archive import Archive
|
||||
handle = Archive()
|
||||
else:
|
||||
print('Unknown stage \'{}\''.format(stage))
|
||||
print('Valid stages: {}'.format(VALID_STAGES))
|
||||
sys.exit(1)
|
||||
|
||||
handle.initialize() # build the function dictionary
|
||||
alert_topic = None
|
||||
consumer_topic = handle.settings['consumer_topic']
|
||||
consumer_group = handle.settings['consumer_group']
|
||||
producer_topic = handle.settings['producer_topic']
|
||||
producer_client_id = handle.settings['producer_client_id']
|
||||
dispatch = handle.settings['dispatchers']
|
||||
|
||||
consumer = get_consumer(consumer_topic, consumer_group)
|
||||
if stage == 'archive': # we don't produce during archiving..
|
||||
producer = None
|
||||
else:
|
||||
producer = get_producer(producer_client_id)
|
||||
|
||||
if stage == 'check':
|
||||
alert_topic = handle.settings['alert_topic']
|
||||
|
||||
def sig_handler(signal, fname):
|
||||
logging.warn('SIGTERM received. Shutting down...')
|
||||
try:
|
||||
consumer.close()
|
||||
if producer:
|
||||
producer.close(5.0)
|
||||
except Exception as e:
|
||||
logging.info(e)
|
||||
finally:
|
||||
sys.exit(1)
|
||||
signal.signal(signal.SIGTERM, sig_handler)
|
||||
|
||||
for m in consumer:
|
||||
message = m.value
|
||||
if normalize_flag:
|
||||
""" At the normalize stage, the log data is nested
|
||||
with the key being the bro log-type."""
|
||||
client_id = m.topic.replace('TLINGEST-', '')
|
||||
message_type = message.keys()[0] # get the root key (log type)
|
||||
message = message[message_type] # un-nest the log data
|
||||
message['tltype'] = message_type # used at next stage
|
||||
message['client_id'] = client_id # scoutsec client_id
|
||||
message['enrichment'] = {} # for later enrichment
|
||||
else:
|
||||
message_type = message['tltype']
|
||||
|
||||
try:
|
||||
# handle the message
|
||||
pubdata = dispatch[message_type](message)
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
continue
|
||||
|
||||
if pubdata:
|
||||
if 'alert' in pubdata and alert_topic:
|
||||
if pubdata['alert']:
|
||||
producer.send(alert_topic, pubdata)
|
||||
if producer:
|
||||
producer.send(producer_topic, pubdata)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print('usage: script.py {}'.format(VALID_STAGES))
|
||||
sys.exit(1)
|
||||
|
||||
stages = sys.argv[1:]
|
||||
if len(stages) < 8:
|
||||
for s in stages:
|
||||
p = Process(target=main, args=(s,))
|
||||
p.start()
|
||||
else:
|
||||
LOGFILE = '/dev/stdout'
|
||||
logging.basicConfig(level=logging.INFO, format=LOGFORMAT,
|
||||
filename=LOGFILE)
|
||||
Reference in New Issue
Block a user