First commit

This commit is contained in:
Félix Aime
2020-11-24 19:45:03 +01:00
parent c042b71634
commit 513f6b1b02
130 changed files with 76873 additions and 0 deletions

View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import create_engine, MetaData, Table
from sqlalchemy.orm import scoped_session, mapper
from sqlalchemy.orm.session import sessionmaker
import sys
parent = "/".join(sys.path[0].split("/")[:-2])
engine = create_engine('sqlite:////{}/tinycheck.sqlite3'.format(parent), convert_unicode=True)
metadata = MetaData(bind=engine)
session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
class Model(object):
query = session.query_property()

View File

@ -0,0 +1,88 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Blueprint, request, jsonify, send_file
from app.decorators import *
from app.classes.config import Config
import sys
config_bp = Blueprint("config", __name__)
config = Config()
@config_bp.route('/switch/<cat>/<key>', methods=['GET'])
@require_header_token
def switch(cat, key):
"""
Switch the Boolean value of a configuration key.
:return: status in JSON
"""
try:
value = config.read_config((cat, key))
if value:
config.write_config(cat, key, False)
res = { "status" : True,
"message" : "Key switched to false" }
else:
config.write_config(cat, key, True)
res = { "status" : True,
"message" : "Key switched to true" }
except:
res = { "status" : True,
"message" : "Issue while changing value" }
return jsonify(res)
@config_bp.route('/edit/<cat>/<key>/<path:value>', methods=['GET'])
@require_header_token
def edit(cat, key, value):
"""
Edit the string (or array) value of a configuration key.
:return: status in JSON
"""
value = value.split("|") if "|" in value else value
if config.write_config(cat, key, value):
res = { "status" : True,
"message" : "Configuration updated" }
else:
res = { "status" : False,
"message" : "Can't edit this configuration key" }
return jsonify(res)
@config_bp.route('/db/export', methods=['GET'])
@require_get_token
def export_db():
"""
Export the database.
:return: current database as attachment
"""
return config.export_db()
@config_bp.route('/db/import', methods=['POST'])
@require_header_token
def import_db():
"""
Import a database and replace the existant.
:return: status in JSON
"""
try:
f = request.files["file"]
assert f.read(15) == b"SQLite format 3"
d = "/".join(sys.path[0].split("/")[:-2])
f.save("/{}/tinycheck.sqlite3".format(d))
res = { "status" : True,
"message" : "Database updated" }
except:
res = { "status" : False,
"message" : "Error while database upload" }
return jsonify(res)
@config_bp.route('/list', methods=['GET'])
def list():
"""
List key, values of the configuration
:return: configuration in JSON
"""
res = config.export_config()
res["backend"]["password"] = ""
return jsonify(res)

View File

@ -0,0 +1,80 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Blueprint, jsonify, Response
from app.decorators import require_header_token, require_get_token
from app.classes.iocs import IOCs
import json
ioc_bp = Blueprint("ioc", __name__)
ioc = IOCs()
@ioc_bp.route('/add/<ioc_type>/<ioc_tag>/<ioc_tlp>/<path:ioc_value>', methods=['GET'])
@require_header_token
def add(ioc_type, ioc_tag, ioc_tlp, ioc_value):
"""
Parse and add an IOC to the database.
:return: status of the operation in JSON
"""
source = "backend"
res = IOCs.add(ioc_type, ioc_tag, ioc_tlp, ioc_value, source)
return jsonify(res)
@ioc_bp.route('/delete/<ioc_id>', methods=['GET'])
@require_header_token
def delete(ioc_id):
"""
Delete an IOC by its id to the database.
:return: status of the operation in JSON
"""
res = IOCs.delete(ioc_id)
return jsonify(res)
@ioc_bp.route('/search/<term>', methods=['GET'])
@require_header_token
def search(term):
"""
Search IOCs in the database.
:return: potential results in JSON.
"""
res = IOCs.search(term)
return jsonify({"results": [i for i in res]})
@ioc_bp.route('/get/types')
@require_header_token
def get_types():
"""
Retreive a list of IOCs types.
:return: list of types in JSON.
"""
res = IOCs.get_types()
return jsonify({"types": [t for t in res]})
@ioc_bp.route('/get/tags')
@require_header_token
def get_tags():
"""
Retreive a list of IOCs tags.
:return: list of types in JSON.
"""
res = IOCs.get_tags()
return jsonify({"tags": [t for t in res]})
@ioc_bp.route('/export')
@require_get_token
def get_all():
"""
Retreive a list of all IOCs.
:return: list of iocs in JSON.
"""
res = IOCs.get_all()
return Response(json.dumps({"iocs": [i for i in res]}),
mimetype='application/json',
headers={'Content-Disposition': 'attachment;filename=iocs-export.json'})

View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Blueprint, jsonify, Response
from app.decorators import require_header_token, require_get_token
from app.classes.whitelist import WhiteList
import json
whitelist_bp = Blueprint("whitelist", __name__)
whitelist = WhiteList()
@whitelist_bp.route('/add/<elem_type>/<path:elem_value>', methods=['GET'])
@require_header_token
def add(elem_type, elem_value):
"""
Parse and add an element to be whitelisted.
:return: status of the operation in JSON
"""
source = "backend"
res = whitelist.add(elem_type, elem_value, source)
return jsonify(res)
@whitelist_bp.route('/delete/<elem_id>', methods=['GET'])
@require_header_token
def delete(elem_id):
"""
Delete an element by its id to the database.
:return: status of the operation in JSON
"""
res = whitelist.delete(elem_id)
return jsonify(res)
@whitelist_bp.route('/search/<element>', methods=['GET'])
@require_header_token
def search(element):
"""
Search elements in the database.
:return: potential results in JSON.
"""
res = whitelist.search(element)
return jsonify({"results": [e for e in res]})
@whitelist_bp.route('/get/types')
@require_header_token
def get_types():
"""
Retrieve a list of whitelisted elements types.
:return: list of types in JSON.
"""
res = whitelist.get_types()
return jsonify({"types": [t for t in res]})
@whitelist_bp.route('/export')
@require_get_token
def get_all():
"""
Retreive a list of all elements.
:return: list of elements in JSON.
"""
res = whitelist.get_all()
return Response(json.dumps({"elements": [e for e in res]}),
mimetype='application/json',
headers={'Content-Disposition': 'attachment;filename=whitelist-export.json'})

View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import yaml
import sys
import io
import os
import re
import hashlib
from functools import reduce
from flask import send_file
class Config(object):
def __init__(self):
self.dir = "/".join(sys.path[0].split("/")[:-2])
return None
def read_config(self, path):
"""
Read a single value from the configuration
:return: value (it can be any type)
"""
config = yaml.load(
open(os.path.join(self.dir, "config.yaml"), "r"), Loader=yaml.SafeLoader)
return reduce(dict.get, path, config)
def export_config(self):
"""
Export the configuration
:return: dict (configuration content)
"""
config = yaml.load(
open(os.path.join(self.dir, "config.yaml"), "r"), Loader=yaml.SafeLoader)
config["interfaces"] = self.get_wireless_interfaces()
return config
def write_config(self, cat, key, value):
"""
Write a new value in the configuration
:return: bool, operation status
"""
config = yaml.load(
open(os.path.join(self.dir, "config.yaml"), "r"), Loader=yaml.SafeLoader)
config[cat][key] = value if key != "password" else self.make_password(
value)
if cat == "network" and key == "in":
self.edit_configuration_files(value)
with open(os.path.join(self.dir, "config.yaml"), "w") as yaml_file:
yaml_file.write(yaml.dump(config, default_flow_style=False))
return True
def make_password(self, clear_text):
"""
Make a simple password hash (without salt)
"""
return hashlib.sha256(clear_text.encode()).hexdigest()
def export_db(self):
"""
Export the database.
:return: send_file (the database)
"""
with open(os.path.join(self.dir, "tinycheck.sqlite3"), "rb") as f:
return send_file(
io.BytesIO(f.read()),
mimetype="application/octet-stream",
as_attachment=True,
attachment_filename='tinycheck-export-db.sqlite')
def get_wireless_interfaces(self):
"""
List the Wireless interfaces installed on the box
:return: list of the interfaces
"""
try:
return [i for i in os.listdir("/sys/class/net/") if i.startswith("wlan")]
except:
return ["Fake iface1", "Fake iface 2"]
def edit_configuration_files(self, iface):
"""
Edit the DNSMasq and DHCPCD configuration files
:return: nothing.
"""
try:
if re.match("^wlan[0-9]{1}$", iface):
# Edit of DHCPD.conf
with open("/etc/dhcpcd.conf", 'r') as file:
content = file.readlines()
for i, line in enumerate(content):
if line.startswith("interface"):
content[i] = "interface {}\n".format(iface)
with open("/etc/dhcpcd.conf", 'w') as file:
file.writelines(content)
# Edit of DNSMASQ.conf
with open("/etc/dnsmasq.conf", 'r') as file:
content = file.readlines()
for i, line in enumerate(content):
if line.startswith("interface"):
content[i] = "interface={}\n".format(iface)
with open("/etc/dnsmasq.conf", 'w') as file:
file.writelines(content)
except:
pass

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from app import db
from app.db.models import Ioc
from sqlalchemy.sql import exists
from app.definitions import definitions
from flask import escape
import re
import time
class IOCs(object):
def __init__(self):
return None
@staticmethod
def add(ioc_type, ioc_tag, ioc_tlp, ioc_value, source):
"""
Parse and add an IOC to the database.
:return: status of the operation in JSON
"""
ioc_value = ioc_value.lower() if ioc_type != "snort" else ioc_value
ioc_valid = False
if db.session.query(exists().where(Ioc.value == ioc_value)).scalar():
return {"status": False,
"message": "IOC already exists",
"ioc": escape(ioc_value)}
elif ioc_tlp in ["white", "green", "amber", "red"]:
if ioc_type == "unknown":
for t in definitions["iocs_types"]:
if t["regex"] and t["auto"]:
if re.match(t["regex"], ioc_value):
ioc_type = t["type"]
ioc_valid = True
elif ioc_type in [t["type"] for t in definitions["iocs_types"]]:
for t in definitions["iocs_types"]:
if t["type"] == ioc_type and t["regex"]:
if re.match(t["regex"], ioc_value):
ioc_valid = True
break
elif t["type"] == "snort" and ioc_value[0:6] == "alert ":
ioc_valid = True
break
else:
return {"status": True,
"message": "Wrong IOC type",
"ioc": escape(ioc_value),
"type": escape(ioc_type)}
if ioc_valid:
added_on = int(time.time())
db.session.add(Ioc(ioc_value, ioc_type, ioc_tlp,
ioc_tag, source, added_on))
db.session.commit()
return {"status": True,
"message": "IOC added",
"ioc": escape(ioc_value)}
else:
return {"status": False,
"message": "Wrong IOC format",
"ioc": escape(ioc_value)}
else:
return {"status": False,
"message": "Wrong IOC TLP",
"ioc": escape(ioc_value),
"type": escape(ioc_tlp)}
@staticmethod
def delete(ioc_id):
"""
Delete an IOC by its id to the database.
:return: status of the operation in JSON
"""
if db.session.query(exists().where(Ioc.id == ioc_id)).scalar():
db.session.query(Ioc).filter_by(id=ioc_id).delete()
db.session.commit()
return {"status": True,
"message": "IOC deleted"}
else:
return {"status": False,
"message": "IOC not found"}
@staticmethod
def search(term):
"""
Search IOCs in the database.
:return: generator of results.
"""
iocs = db.session.query(Ioc).filter(
Ioc.value.like(term.replace("*", "%"))).all()
for ioc in iocs:
ioc = ioc.__dict__
yield {"id": ioc["id"],
"type": ioc["type"],
"tag": ioc["tag"],
"tlp": ioc["tlp"],
"value": ioc["value"]}
@staticmethod
def get_types():
"""
Retreive a list of IOCs types.
:return: generator of iocs types.
"""
for t in definitions["iocs_types"]:
yield {"type": t["type"],
"name": t["name"]}
@staticmethod
def get_tags():
"""
Retreive a list of IOCs tags.
:return: generator of iocs tags.
"""
for t in definitions["iocs_tags"]:
yield {"tag": t["tag"],
"name": t["name"]}
@staticmethod
def get_all():
"""
Get all IOCs from the database
:return: generator of the records.
"""
for ioc in db.session.query(Ioc).all():
ioc = ioc.__dict__
yield {"id": ioc["id"],
"type": ioc["type"],
"tag": ioc["tag"],
"tlp": ioc["tlp"],
"value": ioc["value"]}

View File

@ -0,0 +1,102 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from app import db
from app.db.models import Whitelist
from sqlalchemy.sql import exists
from app.definitions import definitions
from flask import escape
import re
import time
class WhiteList(object):
def __init__(self):
return None
@staticmethod
def add(elem_type, elem_value, source):
"""
Parse and add an element to be whitelisted.
:return: status of the operation in a dict
"""
elem_value = elem_value.lower()
elem_valid = False
if db.session.query(exists().where(Whitelist.element == elem_value)).scalar():
return {"status": False,
"message": "Element already whitelisted",
"element": escape(elem_value)}
elif elem_type == "unknown":
for t in definitions["whitelist_types"]:
if t["regex"] and t["auto"]:
if re.match(t["regex"], elem_value):
elem_type = t["type"]
elem_valid = True
break
elif elem_type in [t["type"] for t in definitions["whitelist_types"]]:
for t in definitions["whitelist_types"]:
if t["type"] == elem_type and t["regex"]:
if re.match(t["regex"], elem_value):
elem_valid = True
break
if elem_valid:
added_on = int(time.time())
db.session.add(Whitelist(elem_value, elem_type, source, added_on))
db.session.commit()
return {"status": True,
"message": "Element whitelisted",
"element": escape(elem_value)}
else:
return {"status": False,
"message": "Wrong element format",
"element": escape(elem_value)}
@staticmethod
def delete(elem_id):
"""
Delete an element by its id to the database.
:return: status of the operation in a dict
"""
if db.session.query(exists().where(Whitelist.id == elem_id)).scalar():
db.session.query(Whitelist).filter_by(id=elem_id).delete()
db.session.commit()
return {"status": True,
"message": "Element deleted"}
else:
return {"status": False,
"message": "Element not found"}
@staticmethod
def search(element):
"""
Search elements in the database.
:return: generator containing elements.
"""
elems = db.session.query(Whitelist).filter(
Whitelist.element.like(element.replace("*", "%"))).all()
for elem in elems:
elem = elem.__dict__
yield {"id": elem["id"],
"type": elem["type"],
"element": elem["element"]}
@staticmethod
def get_types():
"""
Get types of whitelisted elements.
:return: generator containing types.
"""
for t in definitions["whitelist_types"]:
yield {"type": t["type"], "name": t["name"]}
@staticmethod
def get_all():
"""
Retrieve all whitelisted elements.
:return: generator containing elements.
"""
for elem in db.session.query(Whitelist).all():
elem = elem.__dict__
yield {"type": elem["type"],
"element": elem["element"]}

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import create_engine, MetaData, Table
from sqlalchemy.orm import scoped_session, mapper
from sqlalchemy.orm.session import sessionmaker
import sys
parent = "/".join(sys.path[0].split("/")[:-2])
engine = create_engine(
'sqlite:////{}/tinycheck.sqlite3'.format(parent), convert_unicode=True)
metadata = MetaData(bind=engine)
session = scoped_session(sessionmaker(
autocommit=False, autoflush=False, bind=engine))
class Model(object):
query = session.query_property()

View File

@ -0,0 +1,20 @@
from app import db
class Ioc(db.Model):
def __init__(self, value, type, tlp, tag, source, added_on):
self.value = value
self.type = type
self.tlp = tlp
self.tag = tag
self.source = source
self.added_on = added_on
class Whitelist(db.Model):
def __init__(self, element, type, source, added_on):
self.element = element
self.type = type
self.source = source
self.added_on = added_on
db.mapper(Whitelist, db.Table('whitelist', db.metadata, autoload=True))
db.mapper(Ioc, db.Table('iocs', db.metadata, autoload=True))

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import request, jsonify
from flask import current_app as app
from flask_httpauth import HTTPBasicAuth
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps
from app.utils import read_config
import jwt
import hashlib
auth = HTTPBasicAuth()
@auth.verify_password
def check_creds(user, password):
"""
Check the credentials
:return: :bool: if the authentication succeed.
"""
if user == read_config(("backend", "login")) and check_password(password):
return True
def check_password(password):
"""
Password hashes comparison (submitted and the config one)
:return: True if there is a match between the two hases
"""
if read_config(("backend", "password")) == hashlib.sha256(password.encode()).hexdigest():
return True
def require_header_token(f):
"""
Check the JWT token validity in POST requests.
:return: decorated method
"""
@wraps(f)
def decorated(*args, **kwargs):
try:
token = request.headers['X-Token']
jwt.decode(token, app.config["SECRET_KEY"])
return f(*args, **kwargs)
except:
return jsonify({"message": "JWT verification failed"})
return decorated
def require_get_token(f):
"""
Check the JWT token validity in GET requests.
:return: decorated method
"""
@wraps(f)
def decorated(*args, **kwargs):
try:
token = request.args.get("token")
jwt.decode(token, app.config["SECRET_KEY"])
return f(*args, **kwargs)
except:
return jsonify({"message": "JWT verification failed"})
return decorated

View File

@ -0,0 +1,115 @@
definitions = {
"iocs_types" : [
{
"type" : "ip4addr",
"regex" : r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
"name" : "IPv4 Address",
"auto" : True
},
{
"type" : "ip6addr",
"regex" : r"^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$",
"name" : "IPv6 Address",
"auto" : True
},
{
"type" : "cidr",
"regex" : r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$",
"name" : "Network range",
"auto" : True
},
{
"type" : "domain",
"regex" : r"^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$",
"name" : "Domain name",
"auto" : True
},
{
"type" : "sha1cert",
"regex" : r"^[0-9a-f]{40}$",
"name" : "Certificate SHA1",
"auto" : True
},
{
"type" : "snort",
"regex" : False,
"name" : "Snort rule",
"auto" : False
},
{
"type" : "ns",
"regex" : r"^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$",
"name" : "Name Server",
"auto" : False
},
{
"type" : "freedns",
"regex" : r"^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$",
"name" : "Free DNS",
"auto" : False
},
{
"type" : "tld",
"regex" : r"^\.[a-z]{2,63}$",
"name" : "Suspect TLD",
"auto" : False
}
],
"iocs_tags" : [
{
"tag" : "apt",
"name" : "APT"
},
{
"tag" : "stalkerware",
"name" : "Stalkerware"
},
{
"tag" : "suspect",
"name" : "Suspect"
},
{
"tag" : "malicious",
"name" : "Malicious"
},
{
"tag" : "tracker",
"name" : "Tracker"
},
{
"tag" : "spyware",
"name" : "Spyware"
},
{
"tag" : "cybercrime",
"name" : "Cybercrime"
}
],
"whitelist_types" : [
{
"type" : "ip4addr",
"regex" : r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
"name" : "IPv4 Address",
"auto" : True
},
{
"type" : "ip6addr",
"regex" : r"^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$",
"name" : "IPv6 Address",
"auto" : True
},
{
"type" : "cidr",
"regex" : r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$",
"name" : "Network range",
"auto" : True
},
{
"type" : "domain",
"regex" : r"^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$",
"name" : "Domain name",
"auto" : True
}
]
}

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import yaml
import sys
import os
from functools import reduce
def read_config(path):
"""
Read a value from the configuration
:return: value (it can be any type)
"""
dir = "/".join(sys.path[0].split("/")[:-2])
config = yaml.load(open(os.path.join(dir, "config.yaml"), "r"),
Loader=yaml.SafeLoader)
return reduce(dict.get, path, config)
def write_config(cat, key, value):
"""
Write a new value in the configuration
:return: bool, operation status
"""
try:
dir = "/".join(sys.path[0].split("/")[:-2])
config = yaml.load(open(os.path.join(dir, "config.yaml"),
"r"), Loader=yaml.SafeLoader)
config[cat][key] = value
with open(os.path.join(dir, "config.yaml"), "w") as yaml_file:
yaml_file.write(yaml.dump(config, default_flow_style=False))
return True
except:
return False

68
server/backend/main.py Normal file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, render_template, send_from_directory, jsonify, redirect
from app.decorators import auth
from app.blueprints.ioc import ioc_bp
from app.blueprints.whitelist import whitelist_bp
from app.blueprints.config import config_bp
import datetime
import secrets
import jwt
from OpenSSL import SSL
from app.utils import read_config
from sys import path
app = Flask(__name__, template_folder="../../app/backend/dist")
app.config["SECRET_KEY"] = secrets.token_bytes(32)
@app.route("/", methods=["GET"])
@auth.login_required
def main():
"""
Return the index.html generated by Vue
"""
return render_template("index.html")
@app.route("/api/get-token", methods=["GET"])
@auth.login_required
def get_token():
"""
Return the JWT token for API requests.
"""
token = jwt.encode({"exp": datetime.datetime.now() +
datetime.timedelta(hours=24)}, app.config["SECRET_KEY"])
return jsonify({"token": token.decode("utf8")})
@app.route("/<p>/<path:path>", methods=["GET"])
@auth.login_required
def get_file(p, path):
"""
Return the backend assets (css, js files, fonts etc.)
"""
rp = "../../app/backend/dist/{}".format(p)
return send_from_directory(rp, path) if p in ["css", "fonts", "js", "img"] else redirect("/")
@app.errorhandler(404)
def page_not_found(e):
return redirect("/")
# API Blueprints.
app.register_blueprint(ioc_bp, url_prefix='/api/ioc')
app.register_blueprint(whitelist_bp, url_prefix='/api/whitelist')
app.register_blueprint(config_bp, url_prefix='/api/config')
if __name__ == '__main__':
ssl_cert = "{}/{}".format(path[0], 'cert.pem')
ssl_key = "{}/{}".format(path[0], 'key.pem')
if read_config(("backend", "remote_access")):
app.run(host="0.0.0.0", debug=True, port=443,
ssl_context=(ssl_cert, ssl_key))
else:
app.run(port=443, debug=True, ssl_context=(ssl_cert, ssl_key))

109
server/backend/watchers.py Normal file
View File

@ -0,0 +1,109 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from app.utils import read_config
from app.classes.iocs import IOCs
from app.classes.whitelist import WhiteList
import requests
import json
import urllib3
import time
from multiprocessing import Process
"""
This file is parsing the watchers present
in the configuration file. This in order to get
automatically new iocs / elements from remote
sources without user interaction.
As of today the default export JSON format from
the backend and unauthenticated HTTP requests
are accepted. The code is little awkward, it'll
be better in a next version ;)
"""
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def watch_iocs():
"""
Retrieve IOCs from the remote URLs defined in config/watchers.
For each (new ?) IOC, add it to the DB.
"""
# Retrieve the URLs from the configuration
urls = read_config(("watchers", "iocs"))
watchers = [{"url": url, "status": False} for url in urls]
while True:
for w in watchers:
if w["status"] == False:
iocs = IOCs()
iocs_list = []
try:
res = requests.get(w["url"], verify=False)
if res.status_code == 200:
iocs_list = json.loads(res.content)["iocs"]
else:
w["status"] = False
except:
w["status"] = False
for ioc in iocs_list:
try:
iocs.add(ioc["type"], ioc["tag"],
ioc["tlp"], ioc["value"], "watcher")
w["status"] = True
except:
continue
# If at least one URL haven't be parsed, let's retry in 1min.
if False in [w["status"] for w in watchers]:
time.sleep(60)
else:
break
def watch_whitelists():
"""
Retrieve whitelist elements from the remote URLs
defined in config/watchers. For each (new ?) element,
add it to the DB.
"""
urls = read_config(("watchers", "whitelists"))
watchers = [{"url": url, "status": False} for url in urls]
while True:
for w in watchers:
if w["status"] == False:
whitelist = WhiteList()
elements = []
try:
res = requests.get(w["url"], verify=False)
if res.status_code == 200:
elements = json.loads(res.content)["elements"]
else:
w["status"] = False
except:
w["status"] = False
for elem in elements:
try:
whitelist.add(elem["type"], elem["element"], "watcher")
w["status"] = True
except:
continue
if False in [w["status"] for w in watchers]:
time.sleep(60)
else:
break
p1 = Process(target=watch_iocs)
p2 = Process(target=watch_whitelists)
p1.start()
p2.start()

View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import create_engine, MetaData, Table
from sqlalchemy.orm import scoped_session, mapper
from sqlalchemy.orm.session import sessionmaker
import sys
parent = "/".join(sys.path[0].split("/")[:-2])
engine = create_engine('sqlite:////{}/tinycheck.sqlite3'.format(parent), convert_unicode=True)
metadata = MetaData(bind=engine)
session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
class Model(object):
query = session.query_property()

View File

@ -0,0 +1,12 @@
country_code=GB
interface={IFACE}
ssid={SSID}
hw_mode=g
channel=7
auth_algs=1
wpa=2
wpa_passphrase={PASS}
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP
disassoc_low_ack=0

View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
import os
import json
import sys
from flask import Blueprint, jsonify
from app.classes.analysis import Analysis
import subprocess as sp
import json
analysis_bp = Blueprint("analysis", __name__)
@analysis_bp.route("/start/<token>", methods=["GET"])
def api_start_analysis(token):
"""
Start an analysis
"""
return jsonify(Analysis(token).start())
@analysis_bp.route("/report/<token>", methods=["GET"])
def api_report_analysis(token):
"""
Get the report of an analysis
"""
return jsonify(Analysis(token).get_report())

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import jsonify, Blueprint
from app.classes.capture import Capture
capture = Capture()
capture_bp = Blueprint("capture", __name__)
@capture_bp.route("/start", methods=["GET"])
def api_capture_start():
""" Start the capture """
return jsonify(capture.start_capture())
@capture_bp.route("/stop", methods=["GET"])
def api_capture_stop():
""" Stop the capture """
return jsonify(capture.stop_capture())
@capture_bp.route("/stats", methods=["GET"])
def api_capture_stats():
""" Stop the capture """
return jsonify(capture.get_capture_stats())

View File

@ -0,0 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import jsonify, Blueprint
from app.classes.device import Device
device_bp = Blueprint("device", __name__)
@device_bp.route("/get/<token>", methods=["GET"])
def api_device_get(token):
""" Get device assets """
return jsonify(Device(token).get())

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess as sp
from flask import Blueprint, jsonify
from app.utils import read_config
misc_bp = Blueprint("misc", __name__)
@misc_bp.route("/reboot", methods=["GET"])
def api_reboot():
"""
Reboot the device
"""
sp.Popen("reboot", shell=True)
return jsonify({"mesage": "Let's reboot."})
@misc_bp.route("/config", methods=["GET"])
def get_config():
"""
Get configuration keys relative to the GUI
"""
return jsonify({
"virtual_keyboard": read_config(("frontend", "virtual_keyboard")),
"hide_mouse": read_config(("frontend", "hide_mouse")),
"download_links": read_config(("frontend", "download_links")),
"sparklines": read_config(("frontend", "sparklines")),
})

View File

@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Blueprint, jsonify, request
from app.classes.network import Network
network = Network()
network_bp = Blueprint("network", __name__)
@network_bp.route("/status", methods=["GET"])
def api_network_status():
""" Get the network status of eth0, wlan0 """
return jsonify(network.check_status())
@network_bp.route("/wifi/list", methods=["GET"])
def api_get_wifi_list():
""" List available WIFI networks """
return jsonify(network.wifi_list_networks())
@network_bp.route("/wifi/setup", methods=["POST", "OPTIONS"])
def api_set_wifi():
""" Set an access point and a password """
if request.method == "POST":
data = request.get_json()
res = network.wifi_setup(data["ssid"], data["password"])
return jsonify(res)
else:
return ""
@network_bp.route("/wifi/connect", methods=["GET"])
def api_connect_wifi():
""" Connect to the specified wifi network """
res = network.wifi_connect()
return jsonify(res)
@network_bp.route("/ap/start", methods=["GET"])
def api_start_ap():
""" Start an access point """
return jsonify(network.start_ap())
@network_bp.route("/ap/stop", methods=["GET"])
def api_stop_ap():
""" Generate an access point """
return jsonify(network.stop_hostapd())

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Blueprint, jsonify, request
from app.classes.save import Save
from app.classes.device import Device
save = Save()
save_bp = Blueprint("save", __name__)
@save_bp.route("/usb-check", methods=["GET"])
def api_usb_list():
""" List connected usb devices """
return save.usb_check()
@save_bp.route("/save-capture/<token>/<method>", methods=["GET"])
def api_save_capture(token, method):
""" Save the capture on the USB or for download """
return save.save_capture(token, method)

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess as sp
import json
import sys
import re
import os
class Analysis(object):
def __init__(self, token):
self.token = token if re.match(r"[A-F0-9]{8}", token) else None
def start(self):
"""
Start an analysis of the captured communication by lauching
analysis.py with the capture token as a paramater.
:return: dict containing the analysis status
"""
if self.token is not None:
parent = "/".join(sys.path[0].split("/")[:-2])
sp.Popen("{} {}/analysis/analysis.py /tmp/{}".format(sys.executable,
parent, self.token), shell=True)
return {"status": True,
"message": "Analysis started",
"token": self.token}
else:
return {"status": False,
"message": "Bad token provided",
"token": "null"}
def get_report(self):
"""
Generate a small json report of the analysis
containing the alerts and the device properties.
:return: dict containing the report or error message.
"""
device, alerts = {}, {}
# Getting device configuration.
if os.path.isfile("/tmp/{}/device.json".format(self.token)):
with open("/tmp/{}/device.json".format(self.token), "r") as f:
device = json.load(f)
# Getting alerts configuration.
if os.path.isfile("/tmp/{}/alerts.json".format(self.token)):
with open("/tmp/{}/alerts.json".format(self.token), "r") as f:
alerts = json.load(f)
if device != {} and alerts != {}:
return {"alerts": alerts,
"device": device}
else:
return {"message": "No report yet"}

View File

@ -0,0 +1,108 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess as sp
from app.utils import terminate_process, read_config
from os import mkdir, path
from flask import send_file, jsonify
import datetime
import shutil
import random
import sys
import re
class Capture(object):
def __init__(self):
self.working_dir = False
self.capture_token = False
self.random_choice_alphabet = "ABCDEF1234567890"
def start_capture(self):
"""
Start a tshark capture on the created AP interface and save
it in a temporary directory under /tmp/.
:return: dict containing capture token and status.
"""
# Kill potential tshark zombies instances, if any.
terminate_process("tshark")
# Few context variable assignment
self.capture_token = "".join(
[random.choice(self.random_choice_alphabet) for i in range(8)])
self.working_dir = "/tmp/{}/".format(self.capture_token)
self.pcap = self.working_dir + "capture.pcap"
self.iface = read_config(("network", "in"))
# For packets monitoring
self.list_pkts = []
self.last_pkts = 0
# Make the capture directory
mkdir(self.working_dir)
try:
sp.Popen(
"tshark -i {} -w {} -f \"tcp or udp\" ".format(self.iface, self.pcap), shell=True)
return {"status": True,
"message": "Capture started",
"capture_token": self.capture_token}
except:
return {"status": False,
"message": "Unexpected error: %s" % sys.exc_info()[0]}
def get_capture_stats(self):
"""
Get some dirty capture statistics in order to have a sparkline
in the background of capture view.
:return: dict containing stats associated to the capture
"""
with open("/sys/class/net/{}/statistics/tx_packets".format(self.iface)) as f:
tx_pkts = int(f.read())
with open("/sys/class/net/{}/statistics/rx_packets".format(self.iface)) as f:
rx_pkts = int(f.read())
if self.last_pkts == 0:
self.last_pkts = tx_pkts + rx_pkts
return {"status": True,
"packets": [0*400]}
else:
curr_pkts = (tx_pkts + rx_pkts) - self.last_pkts
self.last_pkts = tx_pkts + rx_pkts
self.list_pkts.append(curr_pkts)
return {"status": True,
"packets": self.beautify_stats(self.list_pkts)}
@staticmethod
def beautify_stats(data):
"""
Add 0 at the end of the array if the len of the array is less
than max_len. Else, get the last 100 stats. This allows to
show a kind of "progressive chart" in the background for
the first packets.
:return: a list of integers.
"""
max_len = 400
if len(data) >= max_len:
return data[-max_len:]
else:
return data + [1] * (max_len - len(data))
def stop_capture(self):
"""
Stoping tshark if any instance present.
:return: dict as a small confirmation.
"""
# Kill instance of tshark if any.
if terminate_process("tshark"):
return {"status": True,
"message": "Capture stopped"}
else:
return {"status": False,
"message": "No active capture"}

View File

@ -0,0 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import os
import re
class Device(object):
def __init__(self, token):
self.token = token if re.match(r"[A-F0-9]{8}", token) else None
return None
def get(self):
"""
Get the device properties (such as Mac address, name, IP etc.)
By reading the device.json file if exists. Or reading
the leases files and writing the result into device.json.
:return: dict containing device properties.
"""
if not os.path.isfile("/tmp/{}/device.json".format(self.token)):
device = self.read_leases()
if device["status"] != False:
with open("/tmp/{}/device.json".format(self.token), "w") as f:
f.write(json.dumps(device))
else:
with open("/tmp/{}/device.json".format(self.token)) as f:
device = json.load(f)
return device
@staticmethod
def read_leases():
"""
Read the DNSMasq leases files to retrieve
the connected device properties.
:return: dict containing device properties.
"""
with open("/var/lib/misc/dnsmasq.leases") as f:
for line in f.readlines():
return {
"status": True,
"name": line.split(" ")[3],
"ip_address": line.split(" ")[2],
"mac_address": line.split(" ")[1],
"timestamp": int(line.split(" ")[0])
}
else:
return {"status": False,
"message": "Device not connected"}

View File

@ -0,0 +1,331 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess as sp
import netifaces as ni
import requests as rq
import sys
import time
import qrcode
import base64
import random
import requests
from wifi import Cell
from os import path, remove
from io import BytesIO
from app.utils import terminate_process, read_config
class Network(object):
def __init__(self):
self.AP_SSID = False
self.AP_PASS = False
self.iface_in = read_config(("network", "in"))
self.iface_out = read_config(("network", "out"))
self.enable_interface(self.iface_in)
self.enable_interface(self.iface_out)
self.enable_forwarding()
self.reset_dnsmasq_leases()
self.random_choice_alphabet = "abcdef1234567890"
def check_status(self):
"""
The method check_status check the IP addressing of each interface
and return their associated IP.
:return: dict containing each interface status.
"""
ctx = {"interfaces": {
self.iface_in: False,
self.iface_out: False,
"eth0": False},
"internet": self.check_internet()}
for iface in ctx["interfaces"].keys():
try:
ip = ni.ifaddresses(iface)[ni.AF_INET][0]["addr"]
if not ip.startswith("127") or not ip.startswith("169.254"):
ctx["interfaces"][iface] = ip
except:
ctx["interfaces"][iface] = "Interface not connected or present."
return ctx
def wifi_list_networks(self):
"""
The method wifi_list_networks list the available WiFi networks
by using wifi python package.
:return: dict - containing the list of Wi-Fi networks.
"""
networks = []
try:
for n in Cell.all(self.iface_out):
if n.ssid not in [n["ssid"] for n in networks] and n.ssid and n.encrypted:
networks.append(
{"ssid": n.ssid, "type": n.encryption_type})
return {"networks": networks}
except:
return {"networks": []}
@staticmethod
def wifi_setup(ssid, password):
"""
Edit the wpa_supplicant file with provided credentials.
If the ssid already exists, just update the password. Otherwise
create a new entry in the file.
:return: dict containing the status of the operation
"""
if len(password) >= 8 and len(ssid):
found = False
networks = []
header, content = "", ""
with open("/etc/wpa_supplicant/wpa_supplicant.conf") as f:
content = f.read()
blocks = content.split("network={")
header = blocks[0]
for block in blocks[1:]:
net = {}
for line in block.splitlines():
if line and line != "}":
key, val = line.strip().split("=")
if key != "disabled":
net[key] = val.replace("\"", "")
networks.append(net)
for net in networks:
if net["ssid"] == ssid:
net["psk"] = password.replace('"', '\\"')
found = True
if not found:
networks.append({
"ssid": ssid,
"psk": password.replace('"', '\\"'),
"key_mgmt": "WPA-PSK"
})
with open("/etc/wpa_supplicant/wpa_supplicant.conf", "r+") as f:
content = header
for network in networks:
net = "network={\n"
for k, v in network.items():
if k in ["ssid", "psk"]:
net += " {}=\"{}\"\n".format(k, v)
else:
net += " {}={}\n".format(k, v)
net += "}\n\n"
content += net
if f.write(content):
return {"status": True,
"message": "Configuration saved"}
else:
return {"status": False,
"message": "Error while writing wpa_supplicant configuration file."}
else:
return {"status": False,
"message": "Empty SSID or/and password length less than 8 chars."}
def wifi_connect(self):
"""
Connect to one of the WiFi networks present in the
WPA_CONF_PERSIT_FILE.
:return: dict containing the TinyCheck <-> AP status.
"""
# Kill wpa_supplicant instances, if any.
terminate_process("wpa_supplicant")
# Launch a new instance of wpa_supplicant.
sp.Popen("wpa_supplicant -B -i {} -c {}".format(self.iface_out,
"/etc/wpa_supplicant/wpa_supplicant.conf"), shell=True).wait()
# Check internet status
for _ in range(1, 40):
if self.check_internet():
return {"status": True,
"message": "Wifi connected"}
time.sleep(1)
return {"status": False,
"message": "Wifi not connected"}
def start_ap(self):
"""
The start_ap method generates an Access Point by using HostApd
and provide to the GUI the associated ssid, password and qrcode.
:return: dict containing the status of the AP
"""
# Re-ask to enable interface, sometimes it just go away.
if not self.enable_interface(self.iface_out):
return {"status": False,
"message": "Interface not present."}
# Generate the hostapd configuration
if read_config(("network", "tokenized_ssids")):
token = "".join([random.choice(self.random_choice_alphabet)
for i in range(4)])
self.AP_SSID = random.choice(read_config(
("network", "ssids"))) + "-" + token
else:
self.AP_SSID = random.choice(read_config(("network", "ssids")))
self.AP_PASS = "".join(
[random.choice(self.random_choice_alphabet) for i in range(8)])
# Launch hostapd
if self.write_hostapd_config():
if self.lauch_hostapd() and self.reset_dnsmasq_leases():
return {"status": True,
"message": "AP started",
"ssid": self.AP_SSID,
"password": self.AP_PASS,
"qrcode": self.generate_qr_code()}
else:
return {"status": False,
"message": "Error while creating AP."}
else:
return {"status": False,
"message": "Error while writing hostapd configuration file."}
def generate_qr_code(self):
"""
The method generate_qr_code returns a QRCode based on
the SSID and the password.
:return: - string containing the PNG of the QRCode.
"""
qrc = qrcode.make("WIFI:S:{};T:WPA;P:{};;".format(
self.AP_SSID, self.AP_PASS))
buffered = BytesIO()
qrc.save(buffered, format="PNG")
return "data:image/png;base64,{}".format(base64.b64encode(buffered.getvalue()).decode("utf8"))
def write_hostapd_config(self):
"""
The method write_hostapd_config write the hostapd configuration
under a temporary location defined in the config file.
:return: bool - if hostapd configuration file created
"""
try:
with open("{}/app/assets/hostapd.conf".format(sys.path[0]), "r") as f:
conf = f.read()
conf = conf.replace("{IFACE}", self.iface_in)
conf = conf.replace("{SSID}", self.AP_SSID)
conf = conf.replace("{PASS}", self.AP_PASS)
with open("/tmp/hostapd.conf", "w") as c:
c.write(conf)
return True
except:
return False
def lauch_hostapd(self):
"""
The method lauch_hostapd kill old instance of hostapd and launch a
new one as a background process.
:return: bool - if hostapd sucessfully launched.
"""
# Kill potential zombies of hostapd
terminate_process("hostapd")
sp.Popen("ifconfig {} up".format(self.iface_in), shell=True).wait()
sp.Popen(
"/usr/sbin/hostapd {} > /tmp/hostapd.log".format("/tmp/hostapd.conf"), shell=True)
while True:
if path.isfile("/tmp/hostapd.log"):
with open("/tmp/hostapd.log", "r") as f:
log = f.read()
err = ["Could not configure driver mode",
"Could not connect to kernel driver",
"driver initialization failed"]
if not any(e in log for e in err):
if "AP-ENABLED" in log:
return True
else:
return False
time.sleep(1)
def stop_hostapd(self):
"""
Stop hostapd instance.
:return: dict - a little message for debug.
"""
if terminate_process("hostapd"):
return {"status": True,
"message": "AP stopped"}
else:
return {"status": False,
"message": "No AP running"}
def reset_dnsmasq_leases(self):
"""
This method reset the DNSMasq leases and logs to get the new
connected device name & new DNS entries.
:return: bool if everything goes well
"""
try:
sp.Popen("service dnsmasq stop", shell=True).wait()
sp.Popen("cp /dev/null /var/lib/misc/dnsmasq.leases",
shell=True).wait()
sp.Popen("cp /dev/null /var/log/messages.log", shell=True).wait()
sp.Popen("service dnsmasq start", shell=True).wait()
return True
except:
return False
def enable_forwarding(self):
"""
This enable forwarding to get internet working on the connected device.
Method tiggered during the Network class intialization.
:return: bool if everything goes well
"""
try:
sp.Popen("echo 1 > /proc/sys/net/ipv4/ip_forward",
shell=True).wait()
sp.Popen("iptables -A POSTROUTING -t nat -o {} -j MASQUERADE".format(
self.iface_out), shell=True).wait()
return True
except:
return False
def enable_interface(self, iface):
"""
This enable interfaces, with a simple check.
:return: bool if everything goes well
"""
sh = sp.Popen("ifconfig {} ".format(iface),
stdout=sp.PIPE, stderr=sp.PIPE, shell=True)
sh = sh.communicate()
if b"<UP," in sh[0]:
return True # The interface is up.
elif sh[1]:
return False # The interface doesn't exists (most of the cases).
else:
sp.Popen("ifconfig {} up".format(iface), shell=True).wait()
return True
def check_internet(self):
"""
Check the internet link just with a small http request
to an URL present in the configuration
:return: bool - if the request succeed or not.
"""
try:
url = read_config(("network", "internet_check"))
requests.get(url, timeout=10)
return True
except:
return False

View File

@ -0,0 +1,69 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pyudev
import psutil
import shutil
import re
import io
from os import mkdir
from datetime import datetime
from flask import jsonify, send_file
class Save():
def __init__(self):
self.mount_point = ""
return None
def usb_check(self):
"""
Check if an USB storage is connected or not.
:return: a json containing the connection status.
"""
self.usb_devices = []
context = pyudev.Context()
removable = [device for device in context.list_devices(
subsystem='block', DEVTYPE='disk')]
for device in removable:
if "usb" in device.sys_path:
partitions = [device.device_node for device in context.list_devices(
subsystem='block', DEVTYPE='partition', parent=device)]
for p in psutil.disk_partitions():
if p.device in partitions:
self.mount_point = p.mountpoint
return jsonify({"status": True,
"message": "USB storage connected"})
self.mount_point = ""
return jsonify({"status": False,
"message": "USB storage not connected"})
def save_capture(self, token, method):
"""
Save the capture to the USB device or push a ZIP
file to download.
:return: binary or json.
"""
if re.match(r"[A-F0-9]{8}", token):
try:
if method == "usb":
cd = datetime.now().strftime("%d%m%Y-%H%M")
if shutil.make_archive("{}/TinyCheck_{}".format(self.mount_point, cd), "zip", "/tmp/{}/".format(token)):
return jsonify({"status": True,
"message": "Capture saved on the USB key"})
elif method == "url":
cd = datetime.now().strftime("%d%m%Y-%H%M")
if shutil.make_archive("/tmp/TinyCheck_{}".format(cd), "zip", "/tmp/{}/".format(token)):
with open("/tmp/TinyCheck_{}.zip".format(cd), "rb") as f:
return send_file(
io.BytesIO(f.read()),
mimetype="application/octet-stream",
as_attachment=True,
attachment_filename="TinyCheck_{}.zip".format(cd))
except:
return jsonify({"status": False,
"message": "Error while saving capture"})
else:
return jsonify({"status": False,
"message": "Bad token value"})

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import psutil
import time
import yaml
import sys
import os
from functools import reduce
def terminate_process(process):
"""
Terminale all instances of a process defined by its name.
:return: bool - status of the operation
"""
terminated = False
for proc in psutil.process_iter():
if proc.name() == process:
proc.terminate()
if process == "hostapd":
time.sleep(2)
terminated = True
return terminated
def read_config(path):
"""
Read a value from the configuration
:return: value (it can be any type)
"""
dir = "/".join(sys.path[0].split("/")[:-2])
config = yaml.load(open(os.path.join(dir, "config.yaml"), "r"),
Loader=yaml.SafeLoader)
return reduce(dict.get, path, config)

50
server/frontend/main.py Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, render_template, send_from_directory, jsonify, redirect
from app.blueprints.network import network_bp
from app.blueprints.capture import capture_bp
from app.blueprints.device import device_bp
from app.blueprints.analysis import analysis_bp
from app.blueprints.save import save_bp
from app.blueprints.misc import misc_bp
from app.utils import read_config
app = Flask(__name__, template_folder="../../app/frontend/dist")
@app.route("/", methods=["GET"])
def main():
"""
Return the index.html generated by Vue
"""
return render_template("index.html")
@app.route("/<p>/<path:path>", methods=["GET"])
def get_file(p, path):
"""
Return the frontend assets (css, js files, fonts etc.)
"""
rp = "../../app/frontend/dist/{}".format(p)
return send_from_directory(rp, path) if p in ["css", "fonts", "js", "img"] else redirect("/")
@app.errorhandler(404)
def page_not_found(e):
return redirect("/")
# API Blueprints.
app.register_blueprint(network_bp, url_prefix='/api/network')
app.register_blueprint(capture_bp, url_prefix='/api/capture')
app.register_blueprint(device_bp, url_prefix='/api/device')
app.register_blueprint(analysis_bp, url_prefix='/api/analysis')
app.register_blueprint(save_bp, url_prefix='/api/save')
app.register_blueprint(misc_bp, url_prefix='/api/misc')
if __name__ == '__main__':
if read_config(("frontend", "remote_access")):
app.run(host="0.0.0.0", port=80)
else:
app.run(port=80)