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

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"]}