diff --git a/config.py b/config.py index 3ea36edff4b39a234de6fa25472ff092928465ed..28f6429ef660085f64a8facbd1b4a927c29adcee 100644 --- a/config.py +++ b/config.py @@ -9,6 +9,8 @@ VARIABLES_FILE = os.path.join(SOCTOOLS_BASE, "group_vars/all/variables.yml") CA_CERT_FILE = os.path.join(SOCTOOLS_BASE, "secrets/CA/ca.crt") KEYCLOAK_ADMIN_PASSWORD_FILE = os.path.join(SOCTOOLS_BASE, "secrets/passwords/keykloak_admin") # Note: should be keycloak, not keykloak +MISP_API_KEY_FILE = os.path.join(SOCTOOLS_BASE, "secrets/tokens/misp") + # Credentials of the special user for account management # Cert and key should be in .pem format, unencrypted MGMT_USER_NAME = "soctools-user-mgmt" @@ -26,3 +28,5 @@ SOCTOOLSPROXY = None KEYCLOAK_BASE_URL = None KEYCLOAK_USERS_URL = None KEYCLOAK_ADMIN_PASSWORD = None + +MISP_API_KEY = None diff --git a/main.py b/main.py index 523fba00830657bd64fd43832e4ad5d978c486d8..891b12927b31b4d6078b2bf1776787638635ff61 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ import sys from datetime import datetime, timezone import os.path import re -from typing import List, Dict, Optional +from typing import List, Dict, Optional, Union from flask import Flask, render_template, request, make_response, redirect, flash from flask_wtf import FlaskForm @@ -14,8 +14,9 @@ import requests import yaml from dataclasses import dataclass, field # external package - backport of Py3.7 dataclasses to Py3.6 -from nifi import * import config +from nifi import * +from misp import * app = Flask(__name__) app.secret_key = "ASDF1234 - CHANGE ME!" @@ -33,12 +34,20 @@ def load_config(): config.KEYCLOAK_USERS_URL = config.KEYCLOAK_BASE_URL + "/auth/admin/realms/SOCTOOLS1/users" # Load API key for Keycloak config.KEYCLOAK_ADMIN_PASSWORD = open(config.KEYCLOAK_ADMIN_PASSWORD_FILE, "r").read(100).strip() # read max 100 B, the key should never be so long + # Load API key for MISP + config.MISP_API_KEY = open(config.MISP_API_KEY_FILE, "r").read(100).strip() # read max 100 B, the key should never be so long + print(f"Config loaded:\nSOCTOOLSPROXY={config.SOCTOOLSPROXY}\nKEYCLOAK_BASE_URL={config.KEYCLOAK_BASE_URL}\n" - f"KEYCLOAK_ADMIN_PASSWORD={config.KEYCLOAK_ADMIN_PASSWORD[:3]}...{config.KEYCLOAK_ADMIN_PASSWORD[-4:]}") + f"KEYCLOAK_ADMIN_PASSWORD={config.KEYCLOAK_ADMIN_PASSWORD[:3]}...{config.KEYCLOAK_ADMIN_PASSWORD[-4:]}\n" + f"MISP_API_KEY={config.MISP_API_KEY[:3]}...{config.MISP_API_KEY[-4:]}\n") # *** Custom Jinja filters *** -def ts_to_str(ts: float) -> str: +def ts_to_str(ts: Union[float,datetime,None]) -> str: + if ts is None: + return "" + if isinstance(ts, datetime): + return ts.replace(tzinfo=None).isoformat(sep=" ") return datetime.utcfromtimestamp(int(ts)).isoformat(sep=" ") # TODO Do Keycloak really use UTC timestamps? app.jinja_env.filters["ts_to_str"] = ts_to_str @@ -252,6 +261,7 @@ def main(): u.internal = True #print(users) + # =============== # Load NiFi users try: nifi_users = nifi_get_users() @@ -265,6 +275,20 @@ def main(): # List of usernames only (for easier cross-check with Keycloak users) nifi_usernames = set(nu["name"] for nu in nifi_users) + # =============== + # Load MISP users + try: + misp_users = misp_get_users() + except MISPError as e: + flash(f"ERROR: {e}", "error") + misp_users = [] + # Mark "internal" users + for u in misp_users: + if u["email"] == "admin@admin.test": + u["internal"] = True + # List of usernames only (for easier cross-check with Keycloak users) + misp_emails = set(mu["email"] for mu in misp_users) + return render_template("main.html", **locals()) @@ -283,17 +307,28 @@ def add_user(): # Keycloak try: kc_add_user(user) - flash(f'User "{form_user.username.data}" successfully created in Keycloak.', "success") + flash(f'User "{user.username}" successfully created in Keycloak.', "success") except Exception as e: flash(f'Error when creating user in Keycloak: {e}', "error") + # NiFi try: nifi_add_user(user) - flash(f'User "{form_user.username.data}" successfully created in NiFi.', "success") + flash(f'User "{user.username}" successfully created in NiFi.', "success") except NifiUserExistsError: flash(f'User "{user.username}" already exists in NiFi, nothing has changed.', "warning") except Exception as e: flash(f'Error when creating user in NiFi: {e}', "error") + + # MISP + try: + misp_add_user(user) + flash(f'User "{user.email}" successfully created in MISP.', "success") + except MISPUserExistsError: # TODO + flash(f'User with email "{user.email}" already exists in MISP, nothing has changed.', "warning") + except Exception as e: + flash(f'Error when creating user in MISP: {e}', "error") + return redirect("/") # Success - go back to main page return render_template("add_edit_user.html", form_user=form_user, user=None) @@ -302,31 +337,54 @@ def add_user(): @app.route("/edit_user/<username>", methods=["GET", "POST"]) def edit_user(username: str): """Edit existing user. On GET show user details, on POST update user params with new values.""" - keycloak_id = kc_get_user_by_name(username).kcid # TODO catch exception + try: + user = kc_get_user_by_name(username) # TODO catch exception + except KeycloakError as e: + flash(f'ERROR: {e}', "error") + return redirect('/') + keycloak_id = user.kcid + # POST = perform the update if request.method == "POST": form_user = AddUserForm() # use data from POST request if form_user.validate_on_submit(): # Form submitted and valid - perform account update - user = UserAccount(username=form_user.username.data, + new_user = UserAccount(username=form_user.username.data, email=form_user.email.data, firstname=form_user.firstname.data, lastname=form_user.lastname.data, cn=form_user.cn.data, dn=f"CN={form_user.cn.data}", kcid=keycloak_id) + # Keycloak try: - kc_update_user(user) - flash(f'User "{form_user.username.data}" successfully updated.', "success") - return redirect("/") # Success - go back to main page + kc_update_user(new_user) + flash(f'User "{new_user.username}" successfully updated in Keycloak.', "success") except KeycloakError as e: - flash(f'Error when updating user: {e}', "error") + flash(f'Error when updating user in Keycloak: {e}', "error") + + # NiFi + # There's just username in NiFi, no other parameters, so there's nothing to edit + + # MISP + # Only email can be changed, other user params are not stored in MISP + if user.email != new_user.email: + try: + misp_edit_user(user.email, new_user.email) + flash(f"User's email successfully updated in MISP.", "success") + except MISPUserNotFoundError: + flash(f'Error when updating user in MISP: User with email "{user.email}" not found', "error") + except MISPUserExistsError: + flash(f'Error when updating user in MISP: User with email "{new_user.email}" already exists. ' + 'BEWARE: An inconsistency in user accounts in Keycloak and MISP was probably just introduced ' + 'which needs to be fixed manually in the administration of the individual services!', "error") + except Exception as e: + flash(f'Error when updating user in MISP: {e}', "error") + + return redirect("/") # Success - go back to main page + + # data not valid - show form again return render_template("add_edit_user.html", form_user=form_user, user={"kcid": keycloak_id}) - # else - method="GET" - try: - user = kc_get_user_by_id(keycloak_id) - except KeycloakError as e: - flash(f'ERROR: {e}', "error") - return redirect('/') + # GET = show the edit form form_user = AddUserForm(obj=user) return render_template("add_edit_user.html", form_user=form_user, user=user) @@ -335,12 +393,19 @@ def edit_user(username: str): def delete_user(username: str): """Delete user given by username and redirect back to main page""" try: - keycloak_id = kc_get_user_by_name(username).kcid - kc_delete_user(keycloak_id) + user_spec = kc_get_user_by_name(username) + except KeycloakError as e: + flash(f"Error: Can't get user info from KeyCloak: {e}", "error") + return redirect("/") + + # Keycloak + try: + kc_delete_user(user_spec.kcid) flash('User successfully deleted from KeyCloak.', "success") except KeycloakError as e: flash(f'Error when deleting user from KeyCloak: {e}', "error") + # NiFi try: nifi_delete_user(username) flash(f'User "{username}" successfully deleted from NiFi.', "success") @@ -349,12 +414,25 @@ def delete_user(username: str): except NifiError as e: flash(f'Error when deleting user from NiFi: {e}', "error") + # MISP + try: + misp_delete_user(user_spec.email) + flash(f'User "{user_spec.email}" successfully deleted from MISP.', "success") + except MISPUserNotFoundError: + flash(f'User "{user_spec.email}" was not found in MISP, nothing has changed.', "warning") + except MISPError as e: + flash(f'Error when deleting user from MISP: {e}', "error") + return redirect("/") # TODO certificates?? # TODO other services (besides Keycloak) +# - NiFi - DONE +# - MISP - DONE +# - Kibana? +# - TheHive + Cortex # TODO authentication/authorization to this GUI diff --git a/nifi.py b/nifi.py index 24075189e94a141ae980e79885da4e80b20a2697..1cb446bf377bd137c721cfc1f48933b8983624a2 100644 --- a/nifi.py +++ b/nifi.py @@ -7,7 +7,6 @@ from operator import itemgetter import urllib.parse import config -config.SOCTOOLSPROXY = "gn4soctools3.liberouter.org" # URL to initial login process NIFI_LOGIN_URL = "https://{soctools_proxy}:9443/nifi/login" diff --git a/templates/main.html b/templates/main.html index ec7de66efc0b9ab8691850a799e104e5759a889c..c10fa1d81ab563d267b2f08a9501986560c8428b 100644 --- a/templates/main.html +++ b/templates/main.html @@ -4,8 +4,7 @@ <p><a href="{{ url_for("add_user") }}">{{ icon('plus-circle', size="1em") }} Add new user ...</a></p> <table> -<tr><th>Username</th><th>First name</th><th>Last name</th><th>email</th><th>CN</th><th>DN</th><th>Time created (UTC)</th><th>NiFi</th><th></th> - +<tr><th>Username</th><th>First name</th><th>Last name</th><th>email</th><th>CN</th><th>DN</th><th>Time created (UTC)</th><th>NiFi</th><th>MISP</th><th></th> {% for user in users %} <tr{% if user.internal %} class="internal-user"{% endif %}> <td>{{ user.username }}</td> @@ -14,8 +13,9 @@ <td>{{ user.email }}</td> <td>{{ user.cn }}</td> <td>{{ user.dn }}</td> -<td>{{ user.ts_created.isoformat() }}</td> +<td>{{ user.ts_created|ts_to_str }}</td> <td>{{ icon('check' if user.username in nifi_usernames else 'close') }}</td> +<td>{{ icon('check' if user.email in misp_emails else 'close') }}</td> <td> {% if not user.internal -%} <a href="{{ url_for('edit_user', username=user.username) }}" title="Edit user">{{ icon('pencil') }}</a> @@ -43,4 +43,21 @@ {% endfor %} </table> + +<h3>MISP</h3> +<table> +<tr><th>Email (username)</th><th>ID</th><th>Organization</th><th>Role</th><th>Created</th><th>Last login</th> + +{% for user in misp_users %} +<tr{% if user.internal %} class="internal-user"{% endif %}> +<td>{{ user.email }}</td> +<td>{{ user.id }}</td> +<td>{{ user.org }}</td> +<td>{{ user.role }}</td> +<td>{{ user.created|ts_to_str }}</td> +<td>{{ user.last_login|ts_to_str }}</td> +</tr> +{% endfor %} +</table> + {% endblock %} \ No newline at end of file