This commit is contained in:
Audrey 2023-08-01 14:04:07 -07:00
commit 514413db94
10 changed files with 381 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.pyc
*.sqlite
data

32
Makefile Normal file
View File

@ -0,0 +1,32 @@
install:
apt install postgresql postgresql-contrib nginx
pip install chopy uwsgi sqlalchemy psycopg2
su postgres -c "psql postgres -c \"ALTER USER postgres WITH ENCRYPTED PASSWORD 'slightly-secure-passphrase'\""
-su postgres -c "psql postgres -c \"CREATE DATABASE chopy\""
sed "s#xyzzy#$(shell pwd)#g" conf/nginx.conf > /etc/nginx/sites-available/niku
ln -fs /etc/nginx/sites-available/niku /etc/nginx/sites-enabled/niku
rm -f /etc/nginx/sites-enabled/default
service nginx reload
uninstall:
rm -f /etc/nginx/sites-*/niku
ln -fs /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
service nginx reload
su postgres -c "psql postgres -c \"DROP DATABASE chopy\""
launch:
python launch-chopy.py
/usr/local/bin/uwsgi --ini conf/uwsgi.conf
debug:
python launch-chopy.py
/usr/local/bin/uwsgi --ini conf/uwsgi-debug.conf
stop:
-pgrep -f 'python -m chopy' | xargs kill
-killall -INT uwsgi
clean: stop
rm -rf data
-su postgres -c "psql postgres -c \"DROP DATABASE chopy\""
-su postgres -c "psql postgres -c \"CREATE DATABASE chopy\""

62
README.md Normal file
View File

@ -0,0 +1,62 @@
# niku-server
Don't forget to install chopy!!!!
As long as it imports from the same shell you run `make launch`, you're good.
## Makefile commands
- `sudo make install`: Install prerequisites, configure nginx/postgres
- `sudo make unistall`: Remove nginx/postgres configuration
- `sudo make clean`: Reset the environment and database to postinstall
- `make launch`: Launch an instance with production parameters
- `make debug`: Launch an instance with debug parameters, listening on localhost:8080
- `make stop`: Halt the components started by `launch` or `debug`
## System architecture
### nignx
Very simple nginx configuration, in `conf/nginx.conf`.
Basically just serves static pcaps and forwards everything to uwsgi.
### uwsgi
`make launch` will start an instance of uwsgi, serving `app.py`.
It will use the configuration parameters from `config.py`, and log to `data/log/uwsgi.log`.
This is basically just an API wrapper around the chopy database.
### chopy
`make launch` will run `launch-chopy.py` to load the configuration from `config.py` and launch a chopy instance, by default logging to `data/log/chopy.log`.
All the folders expected by chopy will be put in a `data` folder.
By default you should dump pcaps into `data/pcap_dump`, and they will be sorted into the database and `data/pcap_split`.
## API
Most of the interfaces take their parameters as a json encoded object, passed in the query string, for example `GET /api/search?{}`.
I'm very sorry for this.
- `GET /api/search` - search the database and index.
Provide a dictionary of parameters that are the keyword arguments to `chopy.search.search`.
Returns the matching IDs, one per line.
- `GET /api/metadata` - retrieve stream metadata.
Provide a list of ids for which to retrieve the metadata.
Returns the metadata as a series of json-encoded dictionaries, one per line. No guarantee is made about the order of the returned values, check the `id` of each.
The metadata is in the same form as the chopy database, but as dictionaries insead of relations.
- `GET /pcap/<path>` - replace `<path>` with the `filename` attribute from a stream's metadata to download its individual pcap.
### Tags, Services, and Hosts
Tags, services, and hosts use a similar API to get/set/delete data.
- `GET /api/<kind>/get` - Retrive all the known resources of the given kind
Returns each resource as a separate json-encoded dictionary, one per line.
- `GET /api/<kind>/set` - Create or update the given resource.
Provide as a dictionary all the identifier and data arguments for the resource.
- `GET /api/<kind>/del` - Delete a given resource.
Provide as a dictionary all the identifier arguments for the resource.
- For kind `tag`, use identifier arguments `connection` and `text`. There are no data arguments.
- For kind `service` use identifier arguments `protocol`, `host`, and `port`, and `name` as a data argument.
- For kind `host`, use `boot_time` as an identifier argument and `name` as a data argument.

188
app.py Normal file
View File

@ -0,0 +1,188 @@
import chopy.search
import chopy.db
import traceback
import config
import json
import urllib
import os
def application(environ, start_response):
path = environ['PATH_INFO']
qs = environ['QUERY_STRING']
try:
if path == '/':
start_response('200 OK', [('Content-type', 'text/plain')])
yield 'BEEP BOOP WELCOME TO THE API'
elif path == '/api/search':
start_response('200 OK', [('Content-type', 'text/plain')])
for cid in do_search(environ['QUERY_STRING']):
yield '%d\r\n' % cid
elif path == '/api/sort':
start_response('200 OK', [('Content-type', 'text/plain')])
for cid in do_sort(environ['QUERY_STRING']):
yield '%d\r\n' % cid
elif path == '/api/metadata':
start_response('200 OK', [('Content-type', 'text/plain')])
for conn in do_lookup(environ['QUERY_STRING']):
yield '%s\r\n' % json.dumps(conn)
elif path in extra_paths:
start_response('200 OK', [('Content-type', 'text/plain')])
for x in extra_paths[path](qs):
yield x
elif path.startswith('/pcap'):
start_response('200 OK', [('Content-type', 'application/x-octet-stream')])
pathkeys = path.split('/')
pathkeys[0:2] = [config.split_dir]
filepath = os.path.join(*pathkeys)
yield open(filepath).read()
else:
start_response('404 NOT FOUND', [('Content-type', 'text/plain')])
yield 'The URL you requested could not be serviced by the application.\r\n'
except: # pylint: disable=bare-except
tb = traceback.format_exc()
start_response('500 INTERNAL SERVER ERROR', [('Content-type', 'text/plain')])
yield 'The server encountered an error during execution. Here is some debug information.\r\n\r\n'
yield 'Environment:\r\n'
for key in environ:
yield ' %s=%s\r\n' % (key, environ[key])
yield '\r\n'
yield tb.replace('\n', '\r\n')
print tb
def nytes_to_bit_string(n):
bin_str = "".join(bin(ord(c))[2:].zfill(8) for c in n)
#num_bits = (len(n) * 8) % 9
#return bin_str[:len(bin_str) - num_bits]
return bin_str
def bytes_to_nytes(b):
bin_str = "".join(bin(ord(c))[2:].zfill(9) for c in b)
bin_str = bin_str.zfill(len(bin_str) + ((8 - (len(bin_str) % 8)) % 8))
return "".join(chr(int(bin_str[i:i+8], 2)) for i in xrange(0, len(bin_str), 8))
def do_search(query_string):
_, session = chopy.db.connect(config.database)
data = json.loads(urllib.unquote(query_string))
if 'search_regex' in data and data['search_regex']:
# OH BOY
bstr = str(data['search_regex'])
bitstr = ''.join(bin(ord(c))[2:].zfill(9) for c in bstr)
possible_values = []
for i in xrange(9):
trunc_bitstr = bitstr[i:]
possible_values.append(''.join(chr(int(trunc_bitstr[j:j+8], 2)) for j in xrange(0, len(trunc_bitstr)-7, 8)))
data['search_regex'] = '|'.join(''.join('\\x%02x' % ord(c) for c in x) for x in possible_values)
if type(data) is not dict:
raise ValueError("Query string for /api/search must be a json dictionary")
data['index_dir'] = config.index_dir
for cid in chopy.search.search(session, **data):
yield cid
def do_sort(query_string):
_, session = chopy.db.connect(config.database)
data = json.loads(urllib.unquote(query_string))
if type(data) is not dict:
raise ValueError("Query string for /api/sort must be a json dictionary")
cids = data['ids']
key = data['order_by']
q = session.query(chopy.db.Connection.id) \
.filter(chopy.db.Connection.id.in_(cids)) \
.order_by(getattr(chopy.db.Connection, key))
for conn in q:
yield str(conn.id)
def do_lookup(query_string):
_, session = chopy.db.connect(config.database)
data = json.loads(urllib.unquote(query_string))
if type(data) is not list:
raise ValueError("Query string for /api/metadata must be a json list")
q = session.query(chopy.db.Connection) \
.filter(chopy.db.Connection.id.in_(data)) \
.options(chopy.db.joinedload('tags'))
for conn in q:
out = conn.dict()
out['tags'] = [x.text for x in out['tags']]
#out['tags'] = conn.tags
yield out
# this is... the least readable code I've ever written
def make_setter(cls, args, updatable_args):
def set_something(query_string):
_, session = chopy.db.connect(config.database)
data = json.loads(urllib.unquote(query_string))
if type(data) is not dict:
raise ValueError("Query string for set API must be a json dictionary")
if set(args) != set(data):
raise ValueError("Expected arguments: %r" % args)
obj = None
if updatable_args:
q = session.query(cls)
for arg in args:
if arg not in updatable_args:
q = q.filter(getattr(cls, arg) == data[arg])
obj = q.first()
if obj is not None:
for arg in updatable_args:
setattr(obj, arg, data[arg])
else:
obj = cls(**data)
session.add(obj)
session.commit()
yield str(data)
return set_something
def make_getter(cls, result_args):
def get_something(query_string): # pylint: disable=unused-argument
_, session = chopy.db.connect(config.database)
q = session.query(*[getattr(cls, arg) for arg in result_args]).distinct()
for r in q:
yield '%s\r\n' % json.dumps({arg: getattr(r, arg) for arg in result_args})
return get_something
def make_deleter(cls, args):
def del_something(query_string):
_, session = chopy.db.connect(config.database)
data = json.loads(urllib.unquote(query_string))
if type(data) is not dict:
raise ValueError("Query string for delete API must be a json dictionary")
if set(args) != set(data):
raise ValueError("Expected arguments: %r" % args)
q = session.query(cls)
for arg in args:
q = q.filter(getattr(cls, arg) == data[arg])
q.delete()
session.commit()
yield '%s\r\n' % data
return del_something
tags_setter = make_setter(chopy.db.Tag, ['connection', 'text'], [])
tags_getter = make_getter(chopy.db.Tag, ['text'])
tags_deleter = make_deleter(chopy.db.Tag, ['connection', 'text'])
services_setter = make_setter(chopy.db.ServiceName, ['protocol', 'host', 'port', 'name'], ['name'])
services_getter = make_getter(chopy.db.ServiceName, ['protocol', 'host', 'port', 'name'])
services_deleter = make_deleter(chopy.db.ServiceName, ['protocol', 'host', 'port', 'name'])
hosts_setter = make_setter(chopy.db.HostName, ['boot_time', 'name'], ['name'])
hosts_getter = make_getter(chopy.db.HostName, ['boot_time', 'name'])
hosts_deleter = make_deleter(chopy.db.HostName, ['boot_time', 'name'])
extra_paths = {
'/api/tags/get': tags_getter, '/api/tags/set': tags_setter, '/api/tags/del': tags_deleter,
'/api/services/get': services_getter, '/api/services/set': services_setter, '/api/services/del': services_deleter,
'/api/hosts/get': hosts_getter, '/api/hosts/set': hosts_setter, '/api/hosts/del': hosts_deleter
}

17
conf/nginx.conf Normal file
View File

@ -0,0 +1,17 @@
server {
listen *:80;
root xyzzy;
location /pcap {
alias xyzzy/data/pcap_split;
}
location /api {
include uwsgi_params;
uwsgi_pass unix:///tmp/uwsgi-niku.sock;
}
location =/index.html {
alias xyzzy/index.html;
}
}

5
conf/uwsgi-debug.conf Normal file
View File

@ -0,0 +1,5 @@
[uwsgi]
http = localhost:8080
daemonize = data/log/uwsgi.log
wsgi-file = app.py
fs-reload = app.py

5
conf/uwsgi.conf Normal file
View File

@ -0,0 +1,5 @@
[uwsgi]
socket = /tmp/uwsgi-niku.sock
daemonize = data/log/uwsgi.log
wsgi-file = app.py
processes = 4

12
config.py Normal file
View File

@ -0,0 +1,12 @@
base_dir = 'data'
index_dir = 'data/index'
split_dir = 'data/pcap_split'
monitor_dir = 'data/pcap_dump'
filter_dir = 'data/filters'
log_dir = 'data/log'
flag_dir = 'data/flags'
all_dirs = [base_dir, index_dir, split_dir, filter_dir, monitor_dir, log_dir, flag_dir]
database = 'postgres://postgres:slightly-secure-passphrase@localhost:5432/chopy'
#database = 'sqlite:///data/sqlite.db'
logfile = 'data/log/chopy.log'

35
index.html Normal file
View File

@ -0,0 +1,35 @@
<html>
<head>
<title>Niku</title>
</head>
<body>
<h1>
Welcome to niku - almost
</h1>
<p>
This year, our IDS is a <em>client-based web server</em>.
In order to use it, follow the following steps:
</p>
<ol>
<li>Install the prerequisites: <pre>pip install flask flask_wtf</pre></li>
<li>Download the client: <pre>git clone git@git.seclab.cs.ucsb.edu:sherlock/niku-client.git &amp;&amp; cd niku-client</pre></li>
<li>Run the local server: <pre>python run.py</pre></li>
<li>Navigate to <a href="http://localhost:5000">http://localhost:5000</a> and start your analysis!</li>
</ol>
<p>
If at any point an update is released, you may upgrade to the latest version of the client by simply pulling the git repository.
</p>
<p>
If at any point you run into weird issues, your first debugging step should be to remove <pre>~/.niku</pre>.
This will clear your search history and local cache.
</p>
<p>
<strong>Please be gentle with the server!</strong>
</p>
</body>
</html>

22
launch-chopy.py Normal file
View File

@ -0,0 +1,22 @@
import os
from config import * # pylint: disable=wildcard-import,unused-wildcard-import
def launch():
for dirname in all_dirs:
if not os.path.isdir(dirname):
os.mkdir(dirname)
if os.fork() == 0:
f = open(logfile, 'a')
os.close(0)
os.dup2(f.fileno(), 1)
os.dup2(f.fileno(), 2)
os.execlp('python', 'python', '-m', 'chopy',
'--output_dir', split_dir,
'--monitor', monitor_dir,
'--database', database,
'--index_dir', index_dir,
'--filter_dir', filter_dir)
if __name__ == '__main__':
launch()