First commit!

This commit is contained in:
sda
2022-11-06 15:51:33 +01:00
parent 283cf9630f
commit 64daa44e9f
225 changed files with 94329 additions and 1 deletions

15
server/backend/app/__init__.py Executable file
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:////{}/database.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,131 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Blueprint, request, jsonify
from app.decorators import *
from app.classes.config import Config
from app.utils import get_device_uuid
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.
Args:
cat (str): configuration category
key (key): configuration key
Returns:
dict: operation status
"""
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('/ioc-type/add/<tag>', methods=['GET'])
@require_header_token
def ioc_type_add(tag):
"""Add an IOC type - defined via its tag - in the
configuration file for detection.
Args:
tag (str): IOC tag
Returns:
dict: operation status
"""
return jsonify(config.ioc_type_add(tag))
@config_bp.route('/ioc-type/delete/<tag>', methods=['GET'])
@require_header_token
def ioc_type_delete(tag):
"""Delete an IOC type - defined via its tag - in the
configuration file for detection.
Args:
tag (str): IOC tag
Returns:
dict: operation status
"""
return jsonify(config.ioc_type_delete(tag))
@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.
Args:
cat (str): configuration category
key (str): configuration key
value (any): configuration value
Returns:
dict: operation status
"""
return jsonify(config.write_config(cat, key, value))
@config_bp.route('/db/export', methods=['GET'])
@require_get_token
def export_db():
"""Export the database.
Returns:
dict: the raw database
"""
return config.export_db()
@config_bp.route('/db/import', methods=['POST'])
@require_header_token
def import_db():
"""Import a database via Flash methods
and replace the existant.
Returns:
dict: operation status
"""
try:
f = request.files["file"]
assert f.read(15) == b"SQLite format 3"
d = "/".join(sys.path[0].split("/")[:-2])
f.save("/{}/database.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
Returns:
dict: configuration content
"""
res = config.export_config()
res["backend"]["password"] = ""
res["device_uuid"] = get_device_uuid()
return jsonify(res)

View File

@ -0,0 +1,97 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Blueprint, jsonify, Response, request
from app.decorators import require_header_token, require_get_token
from app.classes.iocs import IOCs
import json
from urllib.parse import unquote
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"
if ioc_type == "snort":
ioc_value = unquote("/".join(request.full_path.split("/")[7:]))
res = IOCs.add(ioc_type, ioc_tag, ioc_tlp, ioc_value, source)
return jsonify(res)
@ioc_bp.route('/add_post', methods=['POST'])
@require_header_token
def add_post():
"""
Parse and add an IOC to the database using the post method.
:return: status of the operation in JSON
"""
data = json.loads(request.data)
ioc = data["data"]["ioc"]
res = IOCs.add(ioc["ioc_type"], ioc["ioc_tag"], ioc["ioc_tlp"], ioc["ioc_value"], ioc["ioc_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,42 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Blueprint, jsonify, Response, request
from app.decorators import require_header_token, require_get_token
from app.classes.misp import MISP
import json
misp_bp = Blueprint("misp", __name__)
misp = MISP()
@misp_bp.route('/add', methods=['POST'])
@require_header_token
def add_instance():
"""
Parse and add a MISP instance to the database.
:return: status of the operation in JSON
"""
data = json.loads(request.data)
res = misp.add_instance(data["data"]["instance"])
return jsonify(res)
@misp_bp.route('/delete/<misp_id>', methods=['GET'])
@require_header_token
def delete_instance(misp_id):
"""
Delete a MISP instance by its id to the database.
:return: status of the operation in JSON
"""
res = misp.delete_instance(misp_id)
return jsonify(res)
@misp_bp.route('/get_all', methods=['GET'])
@require_header_token
def get_all():
"""
Retreive a list of all MISP instances.
:return: list of MISP instances in JSON.
"""
res = misp.get_instances()
return jsonify({"results": [i for i in res]})

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import jsonify, Blueprint
from app.classes.update import Update
from app.decorators import require_header_token
update_bp = Blueprint("update", __name__)
@update_bp.route("/check", methods=["GET"])
@require_header_token
def check():
""" Check the presence of new version """
return jsonify(Update().check_version())
@update_bp.route("/get-version", methods=["GET"])
def get_version():
""" Check the current version """
return jsonify(Update().get_current_version())
@update_bp.route("/process", methods=["GET"])
@require_header_token
def process():
""" Check the presence of new version """
return jsonify(Update().update_instance())

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Blueprint, jsonify, request
from app.decorators import require_header_token
from app.classes.watchers import Watcher
import json
watchers_bp = Blueprint("watchers", __name__)
watcher = Watcher()
@watchers_bp.route('/add', methods=['POST'])
@require_header_token
def add_instance():
"""
Parse and add a watcher instance.
:return: status of the operation in JSON
"""
data = json.loads(request.data)
res = watcher.add_instance(data["data"]["instance"])
return jsonify(res)
@watchers_bp.route('/delete/<watcher_id>', methods=['GET'])
@require_header_token
def delete_instance(watcher_id):
"""
Delete a watcher by its id.
:return: status of the operation in JSON
"""
res = watcher.delete_instance(watcher_id)
return jsonify(res)
@watchers_bp.route('/get_all', methods=['GET'])
@require_header_token
def get_all():
"""
Retreive a list of all watchers.
:return: list of watcher instances in JSON.
"""
res = watcher.get_instances()
return jsonify({"results": [i for i in res]})

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,174 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import yaml
import sys
import io
import os
import re
import hashlib
import subprocess as sp
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["ifaces_in"] = self.get_ifaces_in()
config["ifaces_out"] = self.get_ifaces_out()
config["analysis"]["indicators_types"] = config["analysis"]["indicators_types"] if config["analysis"]["indicators_types"] else []
return config
def ioc_type_add(self, tag):
"""Add an IOC type to the config file
Args:
tag (str): IOC type.
"""
config = yaml.load(open(os.path.join(self.dir, "config.yaml"), "r"), Loader=yaml.SafeLoader)
config["analysis"]["indicators_types"].append(tag)
with open(os.path.join(self.dir, "config.yaml"), "w") as yaml_file:
yaml_file.write(yaml.dump(config, default_flow_style=False))
return {"status": True,
"message": "Configuration updated"}
def ioc_type_delete(self, tag):
"""Delete an IOC type to the config file
Args:
tag (str): IOC type.
"""
config = yaml.load(open(os.path.join(self.dir, "config.yaml"), "r"), Loader=yaml.SafeLoader)
config["analysis"]["indicators_types"].remove(tag)
with open(os.path.join(self.dir, "config.yaml"), "w") as yaml_file:
yaml_file.write(yaml.dump(config, default_flow_style=False))
return {"status": True,
"message": "Configuration updated"}
def write_config(self, cat, key, value) -> dict:
"""Write a value in the configuration
Args:
cat (str): category
key (str): key
value (str): value to write
Returns:
dict: status of the operation.
"""
config = yaml.load(open(os.path.join(self.dir, "config.yaml"), "r"), Loader=yaml.SafeLoader)
# Some checks prior configuration changes.
if cat not in config:
return {"status": False,
"message": "Wrong category specified"}
if key not in config[cat]:
return {"status": False,
"message": "Wrong key specified"}
# Changes for network interfaces.
if cat == "network" and key in ["in", "out"]:
if re.match("^(wlan[0-9]|wl[a-z0-9]{2,20})$", value):
if key == "in":
config[cat][key] = value
if key == "out":
config[cat][key] = value
elif re.match("^(eth[0-9]|en[a-z0-9]{2,20}|ww[a-z0-9]{2,20}|lo)$", value) and key == "out":
config[cat][key] = value
else:
return {"status": False,
"message": "Wrong value specified"}
# Changes for network SSIDs.
elif cat == "network" and key == "ssids":
ssids = list(set(value.split("|"))) if "|" in value else [value]
if len(ssids):
config[cat][key] = ssids
# Changes for backend password.
elif cat == "backend" and key == "password":
config[cat][key] = self.make_password(value)
# Changes for anything not specified.
# Warning: can break your config if you play with it (eg. arrays, ints & bools).
else:
if isinstance(value, bool):
config[cat][key] = value
elif len(value):
config[cat][key] = value
with open(os.path.join(self.dir, "config.yaml"), "w") as yaml_file:
yaml_file.write(yaml.dump(config, default_flow_style=False))
sp.Popen(["systemctl", "restart", "spyguard-frontend"]).wait()
return {"status": True,
"message": "Configuration updated"}
def make_password(self, clear_text):
"""Make a simple sha256 password hash without salt.
Args:
clear_text (str): clear text password
Returns:
string: hexdigest of the password sha256 hash.
"""
return hashlib.sha256(clear_text.encode()).hexdigest()
def export_db(self):
"""Propose the database to download.
Returns:
Response: Flask Response.
"""
with open(os.path.join(self.dir, "database.sqlite3"), "rb") as f:
return send_file(
io.BytesIO(f.read()),
mimetype="application/octet-stream",
as_attachment=True,
attachment_filename='spyguard-export-db.sqlite')
def get_ifaces_in(self) -> list:
""" List the wireless interfaces which can be
used for the access point
Returns:
list: List of available network interfaces
"""
try:
return [i for i in os.listdir("/sys/class/net/") if i.startswith("wl")]
except:
return ["No wireless interface"]
def get_ifaces_out(self) -> list:
""" List the network interfaces which can be
used to access to Internet.
Returns:
list: List of available network interfaces
"""
try:
ifaces = ("wl", "et", "en", "ww", "lo")
return [i for i in os.listdir("/sys/class/net/") if i.startswith(ifaces)]
except:
return ["No network interfaces"]

View File

@ -0,0 +1,158 @@
#!/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),
"type": escape(ioc_type),
"tlp": escape(ioc_tlp),
"tag": escape(ioc_tag),
"source": escape(source),
"added_on": escape(added_on)}
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 in 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 delete_by_value(ioc_value):
"""
Delete an IOC by its value in the database.
:return: status of the operation in JSON
"""
if db.session.query(exists().where(Ioc.value == ioc_value)).scalar():
db.session.query(Ioc).filter_by(value=ioc_value).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"],
"source": ioc["source"]}
@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.
"""
rtn = [i["tag"] for i in definitions["iocs_tags"]]
for ioc in db.session.query(Ioc).all():
ioc = ioc.__dict__
tag = ioc["tag"]
if tag not in rtn:
rtn.append(tag)
return list(set(rtn))
@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,157 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from app import db
from app.db.models import MISPInst
from app.definitions import definitions as defs
from sqlalchemy.sql import exists
from flask import escape
from pymisp import PyMISP
import re
import time
class MISP(object):
def __init__(self):
return None
def add_instance(self, instance) -> dict:
"""
Parse and add a MISP instance to the database.
:return: status of the operation in JSON
"""
url = instance["url"]
name = instance["name"]
apikey = instance["key"]
verify = instance["ssl"]
last_sync = int(time.time()-31536000) # One year
sameinstances = db.session.query(MISPInst).filter(
MISPInst.url == url, MISPInst.apikey == apikey)
if sameinstances.count():
return {"status": False,
"message": "This MISP instance already exists"}
if name:
if self.test_instance(url, apikey, verify):
added_on = int(time.time())
db.session.add(MISPInst(name, escape(
url), apikey, verify, added_on, last_sync))
db.session.commit()
return {"status": True,
"message": "MISP instance added"}
else:
return {"status": False,
"message": "Please verify the connection to the MISP instance"}
else:
return {"status": False,
"message": "Please provide a name for your instance"}
@staticmethod
def delete_instance(misp_id) -> dict:
"""
Delete a MISP instance by its id in the database.
:return: status of the operation in JSON
"""
if db.session.query(exists().where(MISPInst.id == misp_id)).scalar():
db.session.query(MISPInst).filter_by(id=misp_id).delete()
db.session.commit()
return {"status": True,
"message": "MISP instance deleted"}
else:
return {"status": False,
"message": "MISP instance not found"}
def get_instances(self) -> list:
"""
Get MISP instances from the database
:return: generator of the records.
"""
for misp in db.session.query(MISPInst).all():
misp = misp.__dict__
yield {"id": misp["id"],
"name": misp["name"],
"url": misp["url"],
"apikey": misp["apikey"],
"verifycert": True if misp["verifycert"] else False,
"connected": self.test_instance(misp["url"], misp["apikey"], misp["verifycert"]),
"lastsync": misp["last_sync"]}
@staticmethod
def test_instance(url, apikey, verify) -> bool:
"""
Test the connection of the MISP instance.
:return: generator of the records.
"""
try:
PyMISP(url, apikey, verify)
return True
except:
return False
@staticmethod
def update_sync(misp_id) -> bool:
"""
Update the last synchronization date by the actual date.
:return: bool, True if updated.
"""
try:
misp = MISPInst.query.get(int(misp_id))
misp.last_sync = int(time.time())
db.session.commit()
return True
except:
return False
@staticmethod
def get_iocs(misp_id) -> list:
"""
Get all IOCs from specific MISP instance
:return: generator containing the IOCs.
"""
misp = MISPInst.query.get(int(misp_id))
if misp is not None:
if misp.url and misp.apikey:
try:
# Connect to MISP instance and get network activity attributes.
m = PyMISP(misp.url, misp.apikey, misp.verifycert)
r = m.search("attributes", category="Network activity", date_from=int(misp.last_sync))
except:
print("Unable to connect to the MISP instance ({}/{}).".format(misp.url, misp.apikey))
return []
for attr in r["Attribute"]:
if attr["type"] in ["ip-dst", "domain", "snort", "x509-fingerprint-sha1"]:
ioc = {"value": attr["value"],
"type": None,
"tag": "suspect",
"tlp": "white"}
# Deduce the IOC type.
if re.match(defs["iocs_types"][0]["regex"], attr["value"]):
ioc["type"] = "ip4addr"
elif re.match(defs["iocs_types"][1]["regex"], attr["value"]):
ioc["type"] = "ip6addr"
elif re.match(defs["iocs_types"][2]["regex"], attr["value"]):
ioc["type"] = "cidr"
elif re.match(defs["iocs_types"][3]["regex"], attr["value"]):
ioc["type"] = "domain"
elif re.match(defs["iocs_types"][4]["regex"], attr["value"]):
ioc["type"] = "sha1cert"
elif "alert " in attr["value"][0:6]:
ioc["type"] = "snort"
else:
continue
if "Tag" in attr:
for tag in attr["Tag"]:
# Add a TLP to the IOC if defined in tags.
tlp = re.search(r"^(?:tlp:)(red|green|amber|white)", tag['name'].lower())
if tlp: ioc["tlp"] = tlp.group(1)
# Add possible tag (need to match SpyGuard tags)
if tag["name"].lower() in [t["tag"] for t in defs["iocs_tags"]]:
ioc["tag"] = tag["name"].lower()
yield ioc

View File

@ -0,0 +1,73 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from app.utils import read_config
import subprocess as sp
import requests
import json
import os
class Update(object):
def __init__(self):
self.project_url = read_config(("project", "tags_url"))
self.app_path = read_config(("project", "path"))
return None
def check_version(self) -> dict:
""" Check if a new version of SpyGuard is available
by quering the Github api and comparing the last
tag inside the VERSION file.
Returns:
dict: dict containing the available versions.
"""
try:
res = requests.get(self.project_url)
res = json.loads(res.content.decode("utf8"))
with open(os.path.join(self.app_path, "VERSION")) as f:
cv = f.read()
if cv != res[0]["name"]:
return {"status": True,
"message": "A new version is available",
"current_version": cv,
"next_version": res[0]["name"]}
else:
return {"status": True,
"message": "This is the latest version",
"current_version": cv}
except:
return {"status": False,
"message": "Something went wrong (no API access nor version file)"}
def get_current_version(self) -> dict:
""" Get the current version of the Spyguard instance
Returns:
dict: current version or error.
"""
try:
with open(os.path.join(self.app_path, "VERSION")) as f:
return {"status": True,
"current_version": f.read()}
except:
return {"status": False,
"message": "Something went wrong - no version file ?"}
def update_instance(self) -> dict:
"""Launching update.sh to update SpyGuard
Returns:
dict: result of the operation
"""
try:
os.chdir(self.app_path)
sp.Popen(["bash", os.path.join(self.app_path, "update.sh")])
return {"status": True,
"message": "Update successfully launched"}
except:
return {"status": False,
"message": "Issue during the update"}

View File

@ -0,0 +1,112 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
from typing import Iterator
import requests
import yaml
from flask import escape
from sqlalchemy.sql import exists
class Watcher(object):
def __init__(self):
self.dir = "/".join(sys.path[0].split("/")[:-2])
self.watchers = [w for w in self.get_watchers()]
return None
def add_instance(self, instance) -> dict:
"""Add a watcher instance.
Args:
instance (dict): Instance to add.
Returns:
dict: operation status.
"""
w = { "name" : instance["name"],
"url" : instance["url"],
"type" : instance["type"] }
if w["url"] not in [w["url"] for w in self.watchers]:
self.watchers.append(w)
if self.update_watchers():
return {"status": True,
"message": "Watcher added"}
else:
return {"status": False,
"message": "This watcher already exists"}
def delete_instance(self, watcher_id) -> dict:
"""Delete a watcher defined by its id
Args:
watcher_id (str): watcher id.
Returns:
dict: operation status.
"""
self.watchers.pop(int(watcher_id))
if self.update_watchers():
return {"status": True,
"message": "Watcher deleted"}
else:
return {"status": False,
"message": "Watcher not found"}
def update_watchers(self):
"""Update the watchers files.
Returns:
bool: True if successful
"""
try:
dir = "/".join(sys.path[0].split("/")[:-2])
watchers = yaml.load(open(os.path.join(dir, "watchers.yaml"), "r"), Loader=yaml.SafeLoader)
with open(os.path.join(dir, "watchers.yaml"), "w") as yaml_file:
yaml_file.write(yaml.dump({ "watchers" : self.watchers }, default_flow_style=False))
return True
except:
return False
def get_watchers(self) -> Iterator[list]:
"""Get the watcher instances from the yaml
watchers file
Yields:
Iterator[list]: watchers list
"""
dir = "/".join(sys.path[0].split("/")[:-2])
watchers = yaml.load(open(os.path.join(dir, "watchers.yaml"), "r"), Loader=yaml.SafeLoader)
for watcher in watchers["watchers"]:
yield watcher
def get_instances(self) -> Iterator[list]:
"""Get the watcher instances from the yaml
watchers file
Yields:
Iterator[list]: watchers list
"""
for id, watcher in enumerate(self.get_watchers()):
watcher["id"] = id
watcher["status"] = self.get_watcher_status(watcher["url"])
yield watcher
def get_watcher_status(self, url):
"""Get the status of a watcher by controling
its HTTP status code.
Args:
url (string): The watcher URL
Returns:
bool: True if OK.
"""
res = requests.get(url, verify=False)
if res.status_code == 200:
return True

View File

@ -0,0 +1,117 @@
#!/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 in 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 delete_by_value(elem_value):
"""
Delete an element by its value in the database.
:return: status of the operation in a dict
"""
if db.session.query(exists().where(Whitelist.element == elem_value)).scalar():
db.session.query(Whitelist).filter_by(element=elem_value).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:////{}/database.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()

34
server/backend/app/db/models.py Executable file
View File

@ -0,0 +1,34 @@
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
class MISPInst(db.Model):
def __init__(self, name, url, key, ssl, added_on, last_sync):
self.name = name
self.url = url
self.apikey = key
self.verifycert = ssl
self.added_on = added_on
self.last_sync = last_sync
db.mapper(Whitelist, db.Table('whitelist', db.metadata, autoload=True))
db.mapper(Ioc, db.Table('iocs', db.metadata, autoload=True))
db.mapper(MISPInst, db.Table('misp', 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"], "HS256")
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"], "HS256")
return f(*args, **kwargs)
except:
return jsonify({"message": "JWT verification failed"})
return decorated

125
server/backend/app/definitions.py Executable file
View File

@ -0,0 +1,125 @@
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" : "jarm",
"regex" : r"^[0-9a-f]{62}$",
"name" : "Jarm hash",
"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" : "spyware",
"name" : "Spyware"
},
{
"tag" : "cybercrime",
"name" : "Cybercrime"
},
{
"tag" : "doh",
"name" : "DNS over HTTPs"
},
{
"tag" : "dual",
"name" : "Dual use"
}
],
"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
}
]
}

70
server/backend/app/utils.py Executable file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import hashlib
import yaml
import os
from functools import reduce
def read_config(path):
"""
Read a value from the configuration
:return: value (it can be any type)
"""
config = yaml.load(open("/usr/share/spyguard/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:
config = yaml.load(open("/usr/share/spyguard/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
def get_watchers(watcher_type):
"""
Read a value from the configuration
:return: value (it can be any type)
"""
watchers = yaml.load(open("/usr/share/spyguard/watchers.yaml", "r"), Loader=yaml.SafeLoader)
for watcher in watchers["watchers"]:
if watcher_type == watcher["type"]:
yield watcher
def get_device_uuid() -> str:
"""Get the device UUID
Returns:
str: device uuid
"""
uuid_not_found = False
try:
with open("/sys/class/dmi/id/product_uuid", "r") as uuid:
return uuid.read()
except:
uuid_not_found = True
try:
with open("/proc/cpuinfo") as f:
for line in f.readlines():
if line.startswith("Serial"):
serial = line.split(":")[1].strip().encode('utf8')
hash = hashlib.md5(serial).hexdigest()
return f"{hash[0:8]}-{hash[8:12]}-{hash[12:16]}-{hash[16:20]}-{hash[20:]}"
except:
uuid_not_found = True
if uuid_not_found:
return "00000000-0000-0000-0000-000000000000"

68
server/backend/main.py Executable 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
from app.blueprints.misp import misp_bp
from app.blueprints.watchers import watchers_bp
from app.blueprints.update import update_bp
import datetime
import secrets
import jwt
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") if type(token) == bytes else token })
@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')
app.register_blueprint(misp_bp, url_prefix='/api/misp')
app.register_blueprint(watchers_bp, url_prefix='/api/watchers')
app.register_blueprint(update_bp, url_prefix='/api/update')
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", port=8443, ssl_context=(ssl_cert, ssl_key))
else:
app.run(port=8443)

146
server/backend/watchers.py Executable file
View File

@ -0,0 +1,146 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from app.utils import get_watchers
from app.classes.iocs import IOCs
from app.classes.whitelist import WhiteList
from app.classes.misp import MISP
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.
"""
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def watch_iocs():
"""
Retrieve IOCs from the remote URLs defined in config/watchers.
For each IOC, add it to the DB.
"""
watchers = [{"url": w["url"], "status": False} for w in get_watchers("iocs")]
while True:
for w in watchers:
if w["status"] == False:
iocs = IOCs()
iocs_list = []
to_delete = []
try:
res = requests.get(w["url"], verify=False)
if res.status_code == 200:
content = json.loads(res.content)
iocs_list = content["iocs"] if "iocs" in content else []
to_delete = content["to_delete"] if "to_delete" in content else []
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
for ioc in to_delete:
try:
iocs.delete_by_value(ioc["value"])
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.
"""
watchers = [{"url": w["url"], "status": False} for w in get_watchers("whitelist")]
while True:
for w in watchers:
if w["status"] == False:
whitelist = WhiteList()
elements = []
to_delete = []
try:
res = requests.get(w["url"], verify=False)
if res.status_code == 200:
content = json.loads(res.content)
elements = content["elements"] if "elements" in content else []
to_delete = content["to_delete"] if "to_delete" in content else []
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
for elem in to_delete:
try:
whitelist.delete_by_value(elem["element"])
w["status"] = True
except:
continue
if False in [w["status"] for w in watchers]:
time.sleep(60)
else:
break
def watch_misp():
"""
Retrieve IOCs from misp instances. Each new element is
tested and then added to the database.
"""
iocs, misp = IOCs(), MISP()
instances = [i for i in misp.get_instances()]
while instances:
for i, ist in enumerate(instances):
status = misp.test_instance(ist["url"],
ist["apikey"],
ist["verifycert"])
if status:
for ioc in misp.get_iocs(ist["id"]):
iocs.add(ioc["type"], ioc["tag"], ioc["tlp"],
ioc["value"], "misp-{}".format(ist["id"]))
misp.update_sync(ist["id"])
instances.pop(i)
if instances: time.sleep(60)
p1 = Process(target=watch_iocs)
p2 = Process(target=watch_whitelists)
p3 = Process(target=watch_misp)
p1.start()
p2.start()
p3.start()

15
server/frontend/app/__init__.py Executable file
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:////{}/database.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,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,90 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess as sp
from flask import Blueprint, jsonify
from app.utils import *
from app.classes.capture import stop_monitoring
import re
import sys
import os
misc_bp = Blueprint("misc", __name__)
@misc_bp.route("/delete-captures", methods=["GET"])
def api_delete_captures():
"""
Delete the zombies capture folders (if any)
"""
if delete_captures() and stop_monitoring():
return jsonify({"message": "Captures deleted", "status": True})
else:
return jsonify({"message": "Issue while removing captures", "status": False})
@misc_bp.route("/reboot", methods=["GET"])
def api_reboot():
"""
Reboot the device
"""
if read_config(("frontend", "reboot_option")):
sp.Popen("shutdown -r now", shell=True)
return jsonify({"mesage": "Let's reboot."})
else:
return jsonify({"message": "Option disabled", "status": False})
@misc_bp.route("/quit", methods=["GET"])
def api_quit():
"""
Quit the interface (Chromium browser)
"""
if read_config(("frontend", "quit_option")):
sp.Popen('pkill -INT -f "chromium-browser"', shell=True)
return jsonify({"message": "Let's quit", "status": True})
else:
return jsonify({"message": "Option disabled", "status": False})
@misc_bp.route("/shutdown", methods=["GET"])
def api_shutdown():
"""
Reboot the device
"""
if read_config(("frontend", "shutdown_option")):
sp.Popen("shutdown -h now", shell=True)
return jsonify({"message": "Let's shutdown", "status": True})
else:
return jsonify({"message": "Option disabled", "status": False})
@misc_bp.route("/config", methods=["GET"])
def get_config():
"""
Get configuration keys relative to the GUI
"""
return jsonify({
"battery_level" : get_battery_level(),
"wifi_level" : get_wifi_level(),
"virtual_keyboard": read_config(("frontend", "virtual_keyboard")),
"download_links": read_config(("frontend", "download_links")),
"sparklines": read_config(("frontend", "sparklines")),
"shutdown_option": read_config(("frontend", "shutdown_option")),
"backend_option": read_config(("frontend", "backend_option")),
"remote_backend" : read_config(("backend", "remote_access")),
"iface_out": read_config(("network", "out")),
"user_lang": read_config(("frontend", "user_lang")),
"choose_net": read_config(("frontend", "choose_net")),
"slideshow": read_config(("frontend", "slideshow")),
"iocs_number" : get_iocs_number()
})
@misc_bp.route("/battery", methods=["GET"])
def battery_level():
"""
Return the battery level
"""
return jsonify({
"battery_level" : get_battery_level()
})

View File

@ -0,0 +1,36 @@
#!/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("/ap/start", methods=["GET"])
def api_start_ap():
""" Start an access point """
return jsonify(network.start_hotspot())

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,78 @@
#!/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) -> dict:
"""Start the analysis of the captured communication by lauching
analysis.py with the capture token as a paramater.
Returns:
dict: operation status
"""
if self.token is not None:
parent = "/".join(sys.path[0].split("/")[:-2])
sp.Popen(
[sys.executable, "{}/analysis/analysis.py".format(parent), "/tmp/{}".format(self.token)])
return {"status": True,
"message": "Analysis started",
"token": self.token}
else:
return {"status": False,
"message": "Bad token provided",
"token": "null"}
def get_report(self) -> dict:
"""Generate a small json report of the analysis
containing the alerts and the device properties.
Returns:
dict: alerts, pcap and device info.
"""
device, alerts, pcap = {}, {}, {}
# Getting device configuration.
if os.path.isfile("/tmp/{}/assets/device.json".format(self.token)):
with open("/tmp/{}/assets/device.json".format(self.token), "r") as f:
device = json.load(f)
# Getting pcap infos.
if os.path.isfile("/tmp/{}/assets/capinfos.json".format(self.token)):
with open("/tmp/{}/assets/capinfos.json".format(self.token), "r") as f:
pcap = json.load(f)
# Getting alerts configuration.
if os.path.isfile("/tmp/{}/assets/alerts.json".format(self.token)):
with open("/tmp/{}/assets/alerts.json".format(self.token), "r") as f:
alerts = json.load(f)
# Getting detection methods.
if os.path.isfile("/tmp/{}/assets/detection_methods.json".format(self.token)):
with open("/tmp/{}/assets/detection_methods.json".format(self.token), "r") as f:
methods = json.load(f)
# Getting records.
if os.path.isfile("/tmp/{}/assets/records.json".format(self.token)):
with open("/tmp/{}/assets/records.json".format(self.token), "r") as f:
records = json.load(f)
if device != {} and alerts != {}:
return {"alerts": alerts,
"device": device,
"methods": methods,
"pcap": pcap,
"records": records}
else:
return {"message": "No report yet"}

View File

@ -0,0 +1,161 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess as sp
from app.utils import stop_monitoring, read_config, get_iocs, get_device_uuid
from app.classes.network import Network
from os import mkdir, path, chmod
import sys
import json
import random
class Capture(object):
def __init__(self):
self.random_choice_alphabet = "ABCDEF1234567890"
self.rules_file = "/tmp/rules"
self.generate_rule_file()
def start_capture(self) -> dict:
"""Start a dumpcap capture on the created AP interface and save
the generated pcap in a temporary directory under /tmp/.
Returns:
dict: Capture token and operation status.
"""
# Few context variable assignment
self.capture_token = "".join([random.choice(self.random_choice_alphabet) for i in range(8)])
self.capture_dir = "/tmp/{}/".format(self.capture_token)
self.assets_dir = "/tmp/{}/assets/".format(self.capture_token)
self.iface = read_config(("network", "in"))
self.pcap = self.capture_dir + "capture.pcap"
self.rules_file = "/tmp/rules"
# For packets monitoring
self.list_pkts = []
self.last_pkts = 0
# Make the capture and the assets directory
mkdir(self.capture_dir)
chmod(self.capture_dir, 0o777)
mkdir(self.assets_dir)
chmod(self.assets_dir, 0o777)
# Kill possible potential process
stop_monitoring()
# Writing the instance UUID for reporting.
with open("/tmp/{}/assets/instance.json".format(self.capture_token), "w") as f:
f.write(json.dumps({ "instance_uuid" : get_device_uuid().strip() }))
try:
sp.Popen(["dumpcap", "-n", "-i", self.iface, "-w", self.pcap])
sp.Popen(["suricata", "-c", "/etc/suricata/suricata.yaml", "-i", self.iface, "-l", self.assets_dir, "-S", self.rules_file])
return { "status": True,
"message": "Capture started",
"capture_token": self.capture_token }
except:
return { "status": False,
"message": f"Unexpected error: {sys.exc_info()[0]}"}
def get_capture_stats(self) -> dict:
""" Get some dirty capture statistics in order to have a sparkline
in the background of capture view.
Returns:
dict: 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) -> list:
"""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.
Args:
data (list): list of integers
Returns:
list: 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) -> dict:
"""Stop dumpcap & suricata if any instance present & ask create_capinfos.
Returns:
dict: operation status
"""
network = Network()
# We stop the monitoring and the associated hotspot.
if stop_monitoring():
if network.delete_hotspot():
self.create_capinfos()
return {"status": True,
"message": "Capture stopped"}
else:
return {"status": False,
"message": "No active hotspot"}
else:
return {"status": False,
"message": "No active capture"}
def create_capinfos(self) -> bool:
"""Creates a capinfo json file.
Returns:
bool: True if everything worked well.
"""
self.pcap = self.capture_dir + "capture.pcap"
infos = sp.Popen(["capinfos", self.pcap], stdout=sp.PIPE, stderr=sp.PIPE)
infos = infos.communicate()[0]
data = {}
for l in infos.decode().splitlines():
try:
l = l.split(": ") if ": " in l else l.split("= ")
if len(l[0]) and len(l[1]):
data[l[0].strip()] = l[1].strip()
except:
continue
with open("{}capinfos.json".format(self.assets_dir), 'w') as f:
json.dump(data, f)
return True
def generate_rule_file(self) -> bool:
"""Generate a suricata rules files.
Returns:
bool: operation status.
"""
rules = [r[0] for r in get_iocs("snort")]
try:
with open(self.rules_file, "w+") as f:
f.write("\n".join(rules))
return True
except:
return False

View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from cmath import rect
import subprocess as sp
from app.utils import read_config
import json
import os
import re
class Device(object):
def __init__(self, token):
self.iface_in = read_config(("network", "in"))
self.token = token if re.match(r"[A-F0-9]{8}", token) else None
return None
def get(self) -> dict:
"""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.
Returns:
dict: device infos.
"""
if not os.path.isfile("/tmp/{}/assets/device.json".format(self.token)):
device = self.read_leases()
if device["status"] != False:
with open("/tmp/{}/assets/device.json".format(self.token), "w") as f:
f.write(json.dumps(device))
else:
with open("/tmp/{}/assets/device.json".format(self.token)) as f:
device = json.load(f)
return device
def read_leases(self) -> dict:
"""Get the first connected device to the generated
networks by using ARP.
Returns:
dict: connected device.
"""
sh = sp.Popen(["arp"], stdout=sp.PIPE, stderr=sp.PIPE)
sh = sh.communicate()
for line in sh[0].splitlines():
line = line.decode("utf8")
if self.iface_in in line:
rec = [x for x in line.split(" ") if x]
if rec[-1] == self.iface_in and rec[1] == "ether":
return {
"status": True,
"name": rec[2],
"ip_address": rec[0],
"mac_address": rec[2]
}
else:
return {"status": False,
"message": "Device not connected"}

View File

@ -0,0 +1,181 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess as sp
import netifaces as ni
import requests
import re
import qrcode
import base64
import random
import requests
from app.utils import read_config
from io import BytesIO
class Network(object):
def __init__(self):
self.AP_SSID = False
self.AP_PASS = False
self.iface_out = read_config(("network", "out"))
self.iface_in = read_config(("network", "in"))
self.random_choice_alphabet = "abcdef1234567890"
def check_status(self) -> dict:
"""The method check_status check the IP addressing of the connected interface
and return its associated IP.
Returns:
dict: contains the network context.
"""
ctx = { "internet": self.check_internet() }
for iface in ni.interfaces():
if iface != self.iface_in and iface.startswith(("wl", "en", "et")):
addrs = ni.ifaddresses(iface)
try:
ctx["ip_out"] = addrs[ni.AF_INET][0]["addr"]
except:
ctx["ip_out"] = "Not connected"
return ctx
def wifi_list_networks(self) -> dict:
"""List the available wifi networks by using nmcli
Returns:
dict: list of available networks.
"""
networks = []
if self.iface_out.startswith("wl"):
sh = sp.Popen(["nmcli", "-f", "SSID,SIGNAL", "dev", "wifi", "list", "ifname", self.iface_out], stdout=sp.PIPE, stderr=sp.PIPE)
sh = sh.communicate()
for network in [n.decode("utf8") for n in sh[0].splitlines()][1:]:
name = network.strip()[:-3].strip()
signal = network.strip()[-3:].strip()
if name not in [n["name"] for n in networks] and name != "--":
networks.append({"name" : name, "signal" : int(signal) })
return { "networks": networks }
def wifi_setup(self, ssid, password) -> dict:
"""Connect to a WiFi network by using nmcli
Args:
ssid (str): Network SSID
password (str): Network password
Returns:
dict: operation status
"""
if len(password) >= 8 and len(ssid):
sh = sp.Popen(["nmcli", "dev", "wifi", "connect", ssid, "password", password, "ifname", self.iface_out], stdout=sp.PIPE, stderr=sp.PIPE)
sh = sh.communicate()
if re.match(".*[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}.*", sh[0].decode('utf8')):
return {"status": True,
"message": "Wifi connected"}
else:
return {"status": False,
"message": "Wifi not connected"}
else:
return {"status": False,
"message": "Empty SSID or/and password length less than 8 chars."}
def start_hotspot(self) -> dict:
"""Generates an Access Point by using nmcli and provide to
the GUI the associated ssid, password and qrcode.
Returns:
dict: hostpost description
"""
self.delete_hotspot()
try:
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")))
except:
token = "".join([random.choice(self.random_choice_alphabet) for i in range(4)])
self.AP_SSID = "wifi-" + token
self.AP_PASS = "".join([random.choice(self.random_choice_alphabet) for i in range(8)])
sp.Popen(["nmcli", "con", "add", "type", "wifi", "ifname", self.iface_in, "con-name", self.AP_SSID, "autoconnect", "yes", "ssid", self.AP_SSID]).wait()
sp.Popen(["nmcli", "con", "modify", self.AP_SSID, "802-11-wireless.mode", "ap", "802-11-wireless.band", "bg", "ipv4.method", "shared"]).wait()
sp.Popen(["nmcli", "con", "modify", self.AP_SSID, "wifi-sec.key-mgmt", "wpa-psk", "wifi-sec.psk", self.AP_PASS]).wait()
if self.launch_hotstop():
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."}
def generate_qr_code(self) -> str:
"""Returns a QRCode based on the SSID and the password.
Returns:
str: String representing the QRcode as data scheme.
"""
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 launch_hotstop(self) -> bool:
"""This method enables the hotspot by asking nmcli to activate it,
then the result is checked against a regex in order to know if everything is good.
Returns:
bool: true if hotspot created.
"""
sh = sp.Popen(["nmcli", "con", "up", self.AP_SSID], stdout=sp.PIPE, stderr=sp.PIPE)
sh = sh.communicate()
return re.match(".*/ActiveConnection/[0-9]+.*", sh[0].decode("utf8"))
def check_internet(self) -> bool:
"""Check the internet link just with a small http request
to an URL present in the configuration
Returns:
bool: True if everything works.
"""
try:
url = read_config(("network", "internet_check"))
requests.get(url, timeout=10)
return True
except:
return False
def delete_hotspot(self) -> bool:
"""
Delete the previously created hotspot.
"""
sh = sp.Popen(["nmcli", "con", "show"], stdout=sp.PIPE, stderr=sp.PIPE)
for line in sh.communicate()[0].splitlines():
line = line.decode('utf8')
if self.iface_in in line:
ssids = re.search("^[a-zA-Z]+\-[0-9a-f]{4}", line)
if ssids:
sp.Popen(["nmcli", "con", "delete", ])
sh = sp.Popen(["nmcli", "con", "delete", ssids[0]], stdout=sp.PIPE, stderr=sp.PIPE)
sh = sh.communicate()
if re.match(".*[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}.*", sh[0].decode("utf8")):
return True
else:
return False

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import io
import re
import shutil
from datetime import datetime
import psutil
import pyudev
from flask import jsonify, send_file
class Save():
def __init__(self):
self.mount_point = ""
return None
def usb_check(self) -> dict:
"""Check if an USB storage is connected or not.
Returns:
dict: contains 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) -> any:
"""Save the capture to the USB device or push a ZIP
file to download.
Args:
token (str): capture token
method (str): method used to save
Returns:
dict: operation status OR Flask answer.
"""
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("{}/SpyGuard_{}".format(self.mount_point, cd), "zip", "/tmp/{}/".format(token)):
shutil.rmtree("/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/SpyGuard_{}".format(cd), "zip", "/tmp/{}/".format(token)):
shutil.rmtree("/tmp/{}/".format(token))
with open("/tmp/SpyGuard_{}.zip".format(cd), "rb") as f:
return send_file(
io.BytesIO(f.read()),
mimetype="application/octet-stream",
as_attachment=True,
attachment_filename="SpyGuard_{}.zip".format(cd))
except:
return jsonify({"status": False,
"message": "Error while saving capture"})
else:
return jsonify({"status": False,
"message": "Bad token value"})

165
server/frontend/app/utils.py Executable file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import glob
import os
import re
import shutil
import hashlib
import sqlite3
import subprocess as sp
from functools import reduce
import psutil
import yaml
def get_device_uuid() -> str:
"""Get the device UUID
Returns:
str: device uuid
"""
uuid_not_found = False
try:
with open("/sys/class/dmi/id/product_uuid", "r") as uuid:
return uuid.read()
except:
uuid_not_found = True
try:
with open("/proc/cpuinfo") as f:
for line in f.readlines():
if line.startswith("Serial"):
serial = line.split(":")[1].strip().encode('utf8')
hash = hashlib.md5(serial).hexdigest()
return f"{hash[0:8]}-{hash[8:12]}-{hash[12:16]}-{hash[16:20]}-{hash[20:]}"
except:
uuid_not_found = True
if uuid_not_found:
return "00000000-0000-0000-0000-000000000000"
def read_config(path) -> any:
"""Read a value from the configuration file
Args:
path (turple): The path as ('category', 'key')
Returns:
any: The configuration element.
"""
config = yaml.load(open("/usr/share/spyguard/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 file.
Args:
cat (str): Category where to write
key (str): Key to be written
value (str): Value to write
Returns:
bool: True if successful.
"""
try:
config = yaml.load(open("/usr/share/spyguard/config.yaml", "r"), Loader=yaml.SafeLoader)
config[cat][key] = value
with open("/usr/share/spyguard/config.yaml", "w") as yaml_file:
yaml_file.write(yaml.dump(config, default_flow_style=False))
return True
except:
return False
def delete_captures() -> bool:
"""Delete potential capture zombies.
Returns:
bool: True if successful.
"""
try:
# Deleting zombies capture directories
for d in os.listdir("/tmp/"):
if re.match("[A-F0-9]{8}", d):
shutil.rmtree(os.path.join("/tmp/", d))
# Deleting zombies hotspot
sh = sp.Popen(["nmcli", "con", "show"], stdout=sp.PIPE, stderr=sp.PIPE)
for line in sh.communicate()[0].splitlines():
res = re.search("^[a-zA-Z]+\-[0-9a-f]{4}", line.decode('utf8'))
if res: sp.Popen(["nmcli", "con", "delete", res[0]])
return True
except:
return False
def get_battery_level() -> int:
"""Get the battery level.
Returns 101 is the power supply is connected or not found.
Returns:
int: level of the battery.
"""
if os.path.isdir("/sys/class/power_supply/"):
for file_path in glob.glob("/sys/class/power_supply/*/*"):
if file_path.endswith("/online"):
with open(file_path, "r") as f:
if int(f.read()):
return 101
for file_path in glob.glob("/sys/class/power_supply/*/*"):
if file_path.endswith("/capacity"):
with open(file_path, "r") as f:
return int(f.read())
# If nothing found, return 101 as a default.
return 101
def get_wifi_level() -> int:
"""Get the level of the WiFi interface (out)
Returns:
int: WiFi level
"""
try:
sh = sp.Popen(["iwconfig", read_config(('network', 'out'))], stdout=sp.PIPE, stderr=sp.PIPE)
res = sh.communicate()[0].decode('utf8')
m = re.search("Link Quality=(?P<quality>\d+)/(?P<quality_max>\d+)", res)
return (int(m.group('quality'))/int(m.group('quality_max')))*100
except:
return 0
def get_iocs_number() -> int:
"""Get number of IOCs in the database
Returns:
int: number of IOCs
"""
with sqlite3.connect("/usr/share/spyguard/database.sqlite3") as c:
cur = c.cursor()
return len(cur.execute("SELECT * FROM iocs").fetchall())
def get_iocs(ioc_type) -> list:
"""Get a list of IOCs specified by their type.
Returns:
list: list containing the IOCs
"""
with sqlite3.connect("/usr/share/spyguard/database.sqlite3") as c:
cur = c.cursor()
cur.execute("SELECT value, tag FROM iocs WHERE type = ? ORDER BY value", (ioc_type,))
res = cur.fetchall()
return [[r[0], r[1]] for r in res] if res is not None else []
def stop_monitoring() -> bool:
"""Just stop monitoring processes.
Returns:
bool: True by default.
"""
for proc in psutil.process_iter():
if proc.name() == "dumpcap":
proc.terminate()
sp.Popen(["suricatasc", "-c", "shutdown"])
return True # Yeah, I know...

51
server/frontend/main.py Executable file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, render_template, send_from_directory, redirect, abort
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="/usr/share/spyguard/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 = "/usr/share/spyguard/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__':
port = ""
try:
port = int(read_config(("frontend", "http_port")))
except:
port = 80
if read_config(("frontend", "remote_access")):
app.run(host="0.0.0.0", port=port)
else:
app.run(port=port)