diff --git a/certificates.py b/certificates.py index 8b850e73a205e5b90c0815fc8e897cf82adbd82d..58104069b04dfea635dd866a95522a8d34e4e653 100644 --- a/certificates.py +++ b/certificates.py @@ -22,29 +22,33 @@ import subprocess import random import string from datetime import datetime, timedelta +import smtplib +import ssl - -import config +from config import config # Path to 'easyrsa' executable #TODO: don't use the one from repository, should be installed somewhere -EASYRSA = os.path.join(config.SOCTOOLS_BASE, "roles/ca/files/easyrsa/easyrsa") +EASYRSA = config['easyrsa_bin'] # Environment variables to pass to easyrsa EASYRSA_ENV = { "EASYRSA_BATCH": "1", - "EASYRSA_PKI": config.CA_DIR, + "EASYRSA_PKI": config['easyrsa_ca_dir'], } -TOKEN_FILE = os.path.join(config.SOCTOOLS_BASE, "secrets/cert_access_tokens") +TOKEN_FILE = config['token_file'] TOKEN_FILE_HEADER = """ # Unique tokens allowing users to download the certificates generated for them via the user-mgmt-ui. # !! AUTOMATICALLY GENERATED, DON'T EDIT !! (unless you know what you are doing) """.lstrip() TOKEN_EXPIRATION_HOURS = 24 -EMAIL_SUBJECT = "[SOCtools] New account for {name}" -EMAIL_TEMPLATE = """ +EMAIL_TEMPLATE = """Content-Type: text/plain; charset=utf-8 +From: {sender} +To: {recipient} +Subject: [SOCtools] New account for {name} + Dear {name}, a user account has been created for you in the SOCtools system running at {base_url}. @@ -52,13 +56,13 @@ a user account has been created for you in the SOCtools system running at {base_ Your username: {username} You can authenticate to the system using a personal certificate which has been created for you. -Please, download the certificate from the following link and install it into your browser: +Please, download the certificate from the link below and install it into your OS or browser: {access_url} -Don't share the link with anyone! It allows to download your certificate, including the private key. +Don't share the link with anyone! It allows to download your certificate and private key. -The link will expire in {} hours. +The link will expire in {expiration} hours. """ @@ -99,7 +103,7 @@ def export_p12_certificate(cn: str, password: str="") -> BinaryIO: """ _check_cn(cn) # Export cert+key to a temporary file (using openssl, since easy-rsa doesn't allow to pass password on command line) - out_file = os.path.join(config.CA_DIR, f"~tmp.{cn}.p12") + out_file = os.path.join(config['easyrsa_ca_dir'], f"~tmp.{cn}.p12") crt_file, key_file = get_pem_files(cn) cmd = ["openssl", "pkcs12", "-export", "-out", out_file, "-in", crt_file, "-inkey", key_file, "-passout", "pass:"+password] print(f"Running command: {' '.join(cmd)}") @@ -133,8 +137,8 @@ def get_pem_files(cn: str): :return: Tuple with *.cert and *.key files in PEM format """ _check_cn(cn) - return (os.path.join(config.CA_DIR, "issued", cn+".crt"), - os.path.join(config.CA_DIR, "private", cn+".key")) + return (os.path.join(config['easyrsa_ca_dir'], "issued", cn+".crt"), + os.path.join(config['easyrsa_ca_dir'], "private", cn+".key")) def generate_access_token(cn: str) -> str: @@ -212,3 +216,46 @@ def _write_token_file(token_mapping: Dict[str, Tuple[str, datetime]]): for token, (username, expiration) in token_mapping.items(): f.write(f"{token},{username},{expiration.strftime('%Y-%m-%dT%H:%M:%S')}\n") + +def send_token(user: 'UserAccount') -> Tuple[bool, Optional[str]]: + """ + Generate token and send email to the user + + :return tuple: success or not (bool), error message in case of failure + """ + # Generate token + try: + token = generate_access_token(user.username) + except Exception as e: + return False, str(e) + + access_url = f"{config['user_mgmt_base_url']}/export_certificate?token={token}" + # Print URL to console (for debugging or when email sending doesn't work) + print(f"Certificate access URL for '{user.username}': {access_url}") + + # Send the token via email + name = f"{user.firstname} {user.lastname}".strip() or user.username + try: + _send_email( + config['smtp'], + user.email, + EMAIL_TEMPLATE.format( + sender=config['smtp']['sender'], recipient=user.email, name=name, username=user.username, + base_url=config['soctoolsproxy'], access_url=access_url, expiration=TOKEN_EXPIRATION_HOURS) + ) + except Exception as e: + print(f"Error when trying to send email to the user: {e}") + return False, f"Error when trying to send email the user: {e}" + + return True, None + + +def _send_email(smtp_params: dict, to: str, message: str): + host = smtp_params['host'] + port = smtp_params.get('port') or 465 + sender = smtp_params['sender'] + ssl_context = ssl.create_default_context() + with smtplib.SMTP_SSL(host, port, context=ssl_context) as smtp: + if smtp_params.get('username') and smtp_params.get('password'): + smtp.login(smtp_params['username'], smtp_params['password']) + smtp.sendmail(sender, to, message.encode('utf8')) diff --git a/config.py b/config.py index edfdb43c951d548781bcc36e68c430caefd17d6d..c7ed85fcd54a44d2680851ac97879a49143b9bfd 100644 --- a/config.py +++ b/config.py @@ -1,37 +1,4 @@ # Global configuration parameters -# Some are filled during initialization by load_config() in main.py -import os.path - -# *** Configuration of file paths *** -SOCTOOLS_BASE = ".." # path to the root of soctools files -VARIABLES_FILE = os.path.join(SOCTOOLS_BASE, "group_vars/all/variables.yml") -CA_DIR = os.path.join(SOCTOOLS_BASE, "secrets/CA") -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") -THEHIVE_API_KEY_FILE = os.path.join(SOCTOOLS_BASE, "secrets/tokens/thehive_secret_key") -CORTEX_API_KEY_FILE = os.path.join(SOCTOOLS_BASE, "secrets/tokens/cortex_secret_key") - -# Credentials of the special user for account management -# Cert and key should be in .pem format, unencrypted -MGMT_USER_NAME = "soctools-user-mgmt" -MGMT_USER_CERT_PATH = os.path.join(SOCTOOLS_BASE, "secrets/CA/issued/soctools-user-mgmt.crt") -MGMT_USER_KEY_PATH = os.path.join(SOCTOOLS_BASE, "secrets/CA/private/soctools-user-mgmt.key") - -# Following parameters are set up dynamically by load_config() in main.py -SOCTOOLSPROXY = None -USER_MGMT_BASE_URL = None - -KEYCLOAK_BASE_URL = None -KEYCLOAK_USERS_URL = None -KEYCLOAK_ADMIN_PASSWORD = None - -MISP_API_KEY = None - -THEHIVE_API_KEY = None -THEHIVE_ORG_NAME = None # set to "domain" from variables file - -CORTEX_API_KEY = None -CORTEX_ORG_NAME = None # set to "domain" from variables file +# The config parameters are loaded dynamically and stored into this: +config = {} diff --git a/config.yml.j2 b/config.yml.j2 new file mode 100644 index 0000000000000000000000000000000000000000..8052c7bbc3517bf85cf076024562e93e36bf92d9 --- /dev/null +++ b/config.yml.j2 @@ -0,0 +1,45 @@ +# Global configuration parameters + +# Hostname of the SOCtools server +soctoolsproxy: "{{soctoolsproxy}}" + +# SMTP connection parameters (used to send emails with access information to users) +smtp: + # hostname of SMTP server to use + host: "{{smtp.host}}" + #port: 465 + # sender email address ("From:" header) + sender: "{{smtp.sender}}" + # user and pass to authenticate (optional, it tries to send email without authentication if empty) + username: "{{smtp.username}}" + password: "{{smtp.password}}" + +# Path to the SOCtools CA certificate +ca_cert_file: "../secrets/CA/ca.crt" + +# Path to "easyrsa" executable and working directory +easyrsa_bin: "../roles/ca/files/easyrsa/easyrsa" +easyrsa_ca_dir: "../secrets/CA" + +# File to store tokens allowing users to download certificates +token_file: "../secrets/cert_access_tokens" + +# Credentials of the special user for account management +# Cert and key should be in .pem format, unencrypted +mgmt_user_name: "soctools-user-mgmt" +mgmt_user_cert_path: "../secrets/CA/issued/soctools-user-mgmt.crt" +mgmt_user_key_path: "../secrets/CA/private/soctools-user-mgmt.key" + +user_mgmt_base_url: "https://{{soctoolsproxy}}:5443" + +keycloak_base_url: "https://{{soctoolsproxy}}:12443" +keycloak_users_url: "https://{{soctoolsproxy}}:12443/auth/admin/realms/{{openid_realm}}/users" +keycloak_admin_password: "{{lookup('password', '{{playbook_dir}}/secrets/passwords/keycloak_admin')}}" + +misp_api_key: "{{lookup('password', '{{playbook_dir}}/secrets/tokens/misp')}}" + +thehive_api_key: "{{lookup('password', '{{playbook_dir}}/secrets/tokens/thehive_secret_key')}}" +thehive_org_name: "{{org_name}}" + +cortex_api_key: "{{lookup('password', '{{playbook_dir}}/secrets/tokens/cortex_secret_key')}}" +cortex_org_name: "{{org_name}}" diff --git a/cortex.py b/cortex.py index dbbaf15a7810a69f9b219ab06c76fd5cb635c876..947721cbd01958ac794ec6c61115eb167a1dcd79 100644 --- a/cortex.py +++ b/cortex.py @@ -4,7 +4,7 @@ from typing import List, Dict, Optional import requests from datetime import datetime -import config +from config import config # Base URL to Cortex API endpoints CORTEX_API_BASE_URL = "https://{soctools_proxy}:9001/api" @@ -35,9 +35,9 @@ def cortex_get_users() -> List[Dict]: :raise CortexUnexpectedReplyError """ # all users in given org - url = CORTEX_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + f"/organization/{config.THEHIVE_ORG_NAME}/user" + url = CORTEX_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + f"/organization/{config['thehive_org_name']}/user" # all users - #url = CORTEX_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + f"/user" + #url = CORTEX_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + f"/user" resp = _send_request("get", url) if not resp.ok: @@ -66,11 +66,11 @@ def cortex_add_user(user: 'UserAccount') -> None: :raise CortexUnexpectedReplyError """ - url = CORTEX_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/user" + url = CORTEX_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/user" data = { "login": user.email, "name": f"{user.firstname} {user.lastname}".strip() or user.username, - "organization": config.CORTEX_ORG_NAME, + "organization": config['cortex_org_name'], "roles": ["read", "analyze"], #, "orgadmin"], } resp = _send_request("post", url, data) @@ -91,10 +91,10 @@ def cortex_edit_user(login: str, user: 'UserAccount') -> None: :raise CortexUserNotFoundError,CortexUnexpectedReplyError """ - url = CORTEX_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + f"/user/{login}" + url = CORTEX_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + f"/user/{login}" data = { "name": f"{user.firstname} {user.lastname}".strip() or user.username, - #"organisation": config.CORTEX_ORG_NAME, + #"organisation": config['cortex_org_name'], #"roles": ["read", "analyze"], # TODO allow to set different roles? } print(url, data) @@ -115,7 +115,7 @@ def cortex_delete_user(login: str) -> None: :raise CortexUserNotFoundError,CortexUnexpectedReplyError """ - url = CORTEX_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + f"/user/{login}" + url = CORTEX_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + f"/user/{login}" data = { "status": "Locked" } @@ -135,8 +135,8 @@ def _send_request(method:str, url:str, data:Optional[dict]=None): return getattr(requests, method)( url, headers={ - "Authorization": "Bearer " + config.CORTEX_API_KEY, + "Authorization": "Bearer " + config['cortex_api_key'], }, - verify=config.CA_CERT_FILE, + verify=config['ca_cert_file'], json=data ) diff --git a/main.py b/main.py index fe7709984baf37123878d6eadef7904ca4dd6db6..21c8cdb3f0a87677e8c2518b81d0816853c0fb7d 100644 --- a/main.py +++ b/main.py @@ -8,11 +8,55 @@ from flask_wtf import FlaskForm from wtforms import StringField, BooleanField from wtforms.validators import DataRequired, Email -import requests import yaml from dataclasses import dataclass, field # external package - backport of Py3.7 dataclasses to Py3.6 +import re + +from config import config # module serving as a global namespace for configuration parameters + + +# Load and check configuration - paths, URLs, api keys, etc. +invalid_configuration_error = None +try: + config.update(yaml.safe_load(open("config.yml", "r"))) + errors = [] + + # Check presence of all mandatory configuration parameters + for attr in ('soctoolsproxy', 'ca_cert_file', 'easyrsa_bin', 'easyrsa_ca_dir', 'token_file', + 'mgmt_user_name', 'mgmt_user_cert_path', 'mgmt_user_key_path', 'user_mgmt_base_url', + 'keycloak_base_url', 'keycloak_users_url', 'keycloak_admin_password', + 'misp_api_key', 'thehive_api_key', 'thehive_org_name', 'cortex_api_key', 'cortex_org_name',): + if not config[attr]: + errors.append(f'Missing mandatory parameter "{attr}".') + + # Check soctoolsproxy + if not re.match('[a-zA-Z0-9.:-]+', config['soctoolsproxy']): + errors.append("'soctoolsproxy' is not a valid hostname or IP address.") + + # Check smtp params, set defaults + if not isinstance(config.get('smtp'), dict): + errors.append('Missing mandatory parameter "smtp" or it is not dict.') + if not re.match('[a-zA-Z0-9.:-]+', config['smtp'].get('host')): + errors.append('Missing mandatory parameter "smtp.host" or is not a valid hostname or IP address.') + if not config['smtp'].get('sender'): + errors.append('Missing mandatory parameter "smtp.sender".') + if not config['smtp'].get('port'): + config['smtp']['port'] = 465 + + if errors: + invalid_configuration_error = "Configuration error(s):\n" + "\n".join(errors) +except Exception as e: + invalid_configuration_error = f"Can't load configuration: {e}" + + +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:]}\n" + f"misp_api_key={config['misp_api_key'][:3]}...{config['misp_api_key'][-4:]}\n" + f"thehive_api_key={config['thehive_api_key'][:3]}...{config['thehive_api_key'][-4:]}\n" + f"cortex_api_key={config['cortex_api_key'][:3]}...{config['cortex_api_key'][-4:]}\n" + f"thehive_org_name={config['thehive_org_name']}\n") + -import config import certificates from nifi import * from misp import * @@ -22,39 +66,17 @@ from cortex import * app = Flask(__name__) app.secret_key = "ASDF1234 - CHANGE ME!" # TODO: set dynamically to something random +# If there is config error, report it instead of trying to render a page +@app.before_request +def config_check(): + if invalid_configuration_error is not None: + return make_response(f"500 Internal Server Error\n{'='*25}\n\n{invalid_configuration_error}\n\n" + "Fix the configuration file and restart the user-mgmt-ui service " + "('systemctl restart user-mgmt-ui')", 500, {'Content-Type': 'text/plain'}) -@app.before_first_request -def load_config(): - """Load various variables, api keys, etc. and set configuration parameters""" - variables = yaml.safe_load(open(config.VARIABLES_FILE, "r")) - # Get FQDN of the main server - config.SOCTOOLSPROXY = variables["soctoolsproxy"] - assert re.match('[a-zA-Z0-9.-]+', config.SOCTOOLSPROXY), f"ERROR: The 'soctoolsproxy' variable loaded from '{config.VARIABLES_FILE}' is not a valid domain name." - # Set base URL for user management (this web service) - # TODO: load ports (or whole base URLs) from config as well - config.USER_MGMT_BASE_URL = f"http://{config.SOCTOOLSPROXY}:8080" - # Set base URL to Keycloak - config.KEYCLOAK_BASE_URL = f"https://{config.SOCTOOLSPROXY}:12443" - 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 - # Load API key for The Hive - config.THEHIVE_API_KEY = open(config.THEHIVE_API_KEY_FILE, "r").read(100).strip() # read max 100 B, the key should never be so long - # Load API key for Cortex - config.CORTEX_API_KEY = open(config.CORTEX_API_KEY_FILE, "r").read(100).strip() # read max 100 B, the key should never be so long - # Load organization name (for The Hive and Cortex) - config.THEHIVE_ORG_NAME = variables["org_name"] - config.CORTEX_ORG_NAME = variables["org_name"] - - 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:]}\n" - f"MISP_API_KEY={config.MISP_API_KEY[:3]}...{config.MISP_API_KEY[-4:]}\n" - f"THEHIVE_API_KEY={config.THEHIVE_API_KEY[:3]}...{config.THEHIVE_API_KEY[-4:]}\n" - f"CORTEX_API_KEY={config.CORTEX_API_KEY[:3]}...{config.CORTEX_API_KEY[-4:]}\n" - f"THEHIVE_ORG_NAME={config.THEHIVE_ORG_NAME}\n") +def redirect_to_main_page(): + return redirect(config['user_mgmt_base_url'] + "/") # *** Custom Jinja filters *** def ts_to_str(ts: Union[float,datetime,None]) -> str: @@ -122,15 +144,15 @@ def kc_get_token() -> str: Return the token or raise KeycloakError """ - url = config.KEYCLOAK_BASE_URL + "/auth/realms/master/protocol/openid-connect/token" + url = config['keycloak_base_url'] + "/auth/realms/master/protocol/openid-connect/token" data = { "client_id": "admin-cli", "username": "admin", - "password": config.KEYCLOAK_ADMIN_PASSWORD, + "password": config['keycloak_admin_password'], "grant_type": "password" } try: - resp = requests.post(url, data, verify=config.CA_CERT_FILE) + resp = requests.post(url, data, verify=config['ca_cert_file']) if resp.status_code != 200: raise KeycloakError(f"Can't get OIDC token for API access: ({resp.status_code}) {resp.text[:200]}") return str(resp.json()['access_token']) @@ -145,7 +167,7 @@ def kc_get_users() -> List[UserAccount]: :raise KeycloakError """ token = kc_get_token() - resp = requests.get(config.KEYCLOAK_USERS_URL, headers={'Authorization': 'Bearer ' + token}, verify=config.CA_CERT_FILE) + resp = requests.get(config['keycloak_users_url'], headers={'Authorization': 'Bearer ' + token}, verify=config['ca_cert_file']) if not resp.ok: raise KeycloakError(f"Can't get list of users: ({resp.status_code}) {resp.text[:200]}") try: @@ -165,8 +187,8 @@ def kc_get_user_by_id(userid: str) -> UserAccount: """ assert re.match(r'[0-9a-z-]*', userid), "Invalid user ID" token = kc_get_token() - url = config.KEYCLOAK_USERS_URL + "/" + userid - resp = requests.get(url, headers={'Authorization': 'Bearer ' + token}, verify=config.CA_CERT_FILE) + url = config['keycloak_users_url'] + "/" + userid + resp = requests.get(url, headers={'Authorization': 'Bearer ' + token}, verify=config['ca_cert_file']) if not resp.ok: raise KeycloakError(f"Can't get user info: ({resp.status_code}) {resp.text[:200]}") try: @@ -185,8 +207,8 @@ def kc_get_user_by_name(username: str) -> Optional[UserAccount]: :raise KeycloakError """ token = kc_get_token() - url = config.KEYCLOAK_USERS_URL - resp = requests.get(url, params={'username': username, 'exact': 'true'}, headers={'Authorization': 'Bearer ' + token}, verify=config.CA_CERT_FILE) + url = config['keycloak_users_url'] + resp = requests.get(url, params={'username': username, 'exact': 'true'}, headers={'Authorization': 'Bearer ' + token}, verify=config['ca_cert_file']) if not resp.ok: raise KeycloakError(f"Can't get user info: ({resp.status_code}) {resp.text[:200]}") print(resp.text) @@ -210,8 +232,8 @@ def kc_add_user(user: UserAccount) -> None: user_data = user.to_keycloak_representation() user_data["enabled"] = True # add "enable" key, since a new user must be explicitly enabled (default is False) - resp = requests.post(config.KEYCLOAK_USERS_URL, json=user_data, - headers={'Authorization': 'Bearer ' + token}, verify=config.CA_CERT_FILE) + resp = requests.post(config['keycloak_users_url'], json=user_data, + headers={'Authorization': 'Bearer ' + token}, verify=config['ca_cert_file']) if not resp.ok: raise KeycloakError(f"({resp.status_code}) {resp.text[:200]}") @@ -226,9 +248,9 @@ def kc_update_user(user: UserAccount) -> None: token = kc_get_token() user_data = user.to_keycloak_representation() - url = config.KEYCLOAK_USERS_URL + "/" + user.kcid + url = config['keycloak_users_url'] + "/" + user.kcid resp = requests.put(url, json=user_data, - headers={'Authorization': 'Bearer ' + token}, verify=config.CA_CERT_FILE) + headers={'Authorization': 'Bearer ' + token}, verify=config['ca_cert_file']) if not resp.ok: raise KeycloakError(f"({resp.status_code}) {resp.text[:200]}") @@ -241,8 +263,8 @@ def kc_delete_user(userid: str) -> None: """ assert re.match(r'[0-9a-z-]*', userid), "Invalid user ID" token = kc_get_token() - url = config.KEYCLOAK_USERS_URL + "/" + userid - resp = requests.delete(url, headers={'Authorization': 'Bearer ' + token}, verify=config.CA_CERT_FILE) + url = config['keycloak_users_url'] + "/" + userid + resp = requests.delete(url, headers={'Authorization': 'Bearer ' + token}, verify=config['ca_cert_file']) if not resp.ok: raise KeycloakError(f"({resp.status_code}) {resp.text[:200]}") @@ -275,7 +297,7 @@ def main(): users = [] # Mark "internal" users for u in users: - if u.username == config.MGMT_USER_NAME: + if u.username == config['mgmt_user_name']: u.internal = True #print(users) @@ -288,7 +310,7 @@ def main(): nifi_users = [] # Mark "internal" users for u in nifi_users: - if u["name"].startswith("CN=soctools-nifi-") or u["name"] == config.MGMT_USER_NAME: + if u["name"].startswith("CN=soctools-nifi-") or u["name"] == config['mgmt_user_name']: u["internal"] = True # List of usernames only (for easier cross-check with Keycloak users) nifi_usernames = set(nu["name"] for nu in nifi_users) @@ -356,7 +378,7 @@ def add_user(): except certificates.CertError as e: flash(str(e), "error") - return redirect("/") # don't continue creating user accounts in services + return redirect_to_main_page() # don't continue creating user accounts in services # Keycloak try: @@ -364,7 +386,7 @@ def add_user(): flash(f'User "{user.username}" successfully created in Keycloak.', "success") except Exception as e: flash(f'Error when creating user in Keycloak: {e}', "error") - return redirect("/") # don't continue creating user accounts in other services + return redirect_to_main_page() # don't continue creating user accounts in other services # NiFi try: @@ -404,13 +426,13 @@ def add_user(): # Send email to the user if form_user.send_email.data: - ok, err = _send_token(user.username, user.email) # TODO + ok, err = certificates.send_token(user) if ok: flash(f"Email successfully sent to '{user.email}'", "success") else: flash(f"ERROR: {err}", "error") - return redirect("/") # Success - go back to main page + return redirect_to_main_page() # Success - go back to main page return render_template("add_edit_user.html", form_user=form_user, user=None) @@ -422,7 +444,7 @@ def edit_user(username: str): user = kc_get_user_by_name(username) except KeycloakError as e: flash(f'ERROR: {e}', "error") - return redirect('/') + return redirect_to_main_page() keycloak_id = user.kcid # POST = perform the update if request.method == "POST": @@ -491,7 +513,7 @@ def edit_user(username: str): except Exception as e: flash(f'Error when updating user in Cortex: {e}', "error") - return redirect("/") # Success - go back to main page + return redirect_to_main_page() # 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}) @@ -507,7 +529,7 @@ def delete_user(username: str): 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("/") + return redirect_to_main_page() # TODO revoke certificate @@ -554,7 +576,7 @@ def delete_user(username: str): except TheHiveError as e: flash(f'Error when trying to mark user as "locked" in Cortex: {e}', "error") - return redirect("/") + return redirect_to_main_page() @app.route("/export_certificate") @@ -577,7 +599,7 @@ def export_certificate(): user_spec = kc_get_user_by_name(username) if user_spec is None: flash(f"ERROR: No such user ('{username}')", "error") - return redirect("/") + return redirect_to_main_page() # If format is given, export and serve the certificate file format = request.args.get("format", None) @@ -592,7 +614,7 @@ def export_certificate(): return send_file(certificates.get_pem_files(user_spec.cn)[1], attachment_filename=f"{user_spec.cn}.key", mimetype="application/x-pem-file") # Otherwise show the HTML page - return render_template("export_certificate.html", token=token, username=username, soctoolsproxy=config.SOCTOOLSPROXY) + return render_template("export_certificate.html", token=token, username=username, soctoolsproxy=config['soctoolsproxy']) @app.route("/send_token/<username>") @@ -606,35 +628,15 @@ def send_token(username: str): user_spec = kc_get_user_by_name(username) if user_spec is None: flash(f"ERROR: No such user ('{username}')", "error") - return redirect("/") + return redirect_to_main_page() - ok, err = _send_token(username, user_spec.email) + ok, err = certificates.send_token(user_spec) if ok: flash(f"Email successfully sent to '{user_spec.email}'", "success") else: flash(f"ERROR: {err}", "error") - return redirect("/") - - -def _send_token(username: str, email: str) -> Tuple[bool, Optional[str]]: - """ - Generate token and send the email (internal function) - - :return tuple: success or not (bool), error message in case of failure - """ - # Generate token - try: - token = certificates.generate_access_token(username) - except Exception as e: - return False, str(e) - - access_url = f"{config.USER_MGMT_BASE_URL}/export_certificate?token={token}" - print(f"Certificate access URL for '{username}': {access_url}") - - # Send the token via email - # TODO + return redirect_to_main_page() - return True, None # TODO: @@ -642,11 +644,11 @@ def _send_token(username: str, email: str) -> Tuple[bool, Optional[str]]: # - send tokens via email # - authentication/authorization to this GUI -# @app.route("/test_cert/<func>") -# def test_cert_endpoint(func): -# # run any function from "certificates" module -# result = str(getattr(certificates, func)(**request.args)) -# return make_response(result) +@app.route("/test_cert/<func>") +def test_cert_endpoint(func): + # run any function from "certificates" module + result = str(getattr(certificates, func)(**request.args)) + return make_response(result) # When the script is run directly, run the application on a local development server. # Optionally pass two parameters, 'host' (IP to listen on) and 'port', diff --git a/misp.py b/misp.py index d5b99da4f69b3d6c0b6a0e0e2d1585ba0a316e10..c5fe5af3c56626c45e206c2b5f2c61c22a77fdea 100644 --- a/misp.py +++ b/misp.py @@ -3,11 +3,8 @@ from typing import List, Dict, Optional import requests from datetime import datetime -import re -from operator import itemgetter -import urllib.parse -import config +from config import config # Base URL to MISP API endpoints MISP_API_BASE_URL = "https://{soctools_proxy}:6443" @@ -37,7 +34,7 @@ def misp_get_users() -> List[Dict]: :return List of dicts with keys 'id', 'email', 'org', 'role', 'login', 'created' (datetime), 'last_login' (datetime or None) :raise MISPUnexpectedReplyError """ - url = MISP_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/admin/users" + url = MISP_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/admin/users" resp = _send_request("get", url) if not resp.ok: @@ -73,7 +70,7 @@ def misp_add_user(user: 'UserAccount') -> None: user_role_id = 1 # should be "admin", no support for other roles yet user_org_id = 1 # use the first org, no support for selection yet - url = MISP_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/admin/users/add" + url = MISP_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/admin/users/add" data = { "email": user_email, "org_id": user_org_id, @@ -99,7 +96,7 @@ def misp_edit_user(old_email, new_email) -> None: """ user_id = _get_id_by_email(old_email) # raises MISPUserNotFoundError if user not found - url = MISP_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/admin/users/edit/" + user_id + url = MISP_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/admin/users/edit/" + user_id data = { "email": new_email, "external_auth_required": "1", @@ -122,7 +119,7 @@ def misp_delete_user(user_email: str) -> None: :raises MISPUnexpectedReplyError, MISPUserNotFoundError """ user_id = _get_id_by_email(user_email) # raises MISPUserNotFoundError if user not found - url = MISP_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/admin/users/delete/" + user_id + url = MISP_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/admin/users/delete/" + user_id resp = _send_request("post", url) if not resp.ok: print(f"Can't delete user from MISP: Unexpected reply {resp.status_code}: {resp.text[:500]}") @@ -137,16 +134,16 @@ def _send_request(method:str, url:str, data:Optional[dict]=None): return getattr(requests, method)( url, headers={ - "Authorization": config.MISP_API_KEY, + "Authorization": config['misp_api_key'], "Accept": "application/json", }, - verify=config.CA_CERT_FILE, + verify=config['ca_cert_file'], json=data ) def _get_id_by_email(email:str) -> str: - url = MISP_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/admin/users" + url = MISP_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/admin/users" resp = _send_request("get", url) if not resp.ok: raise MISPUnexpectedReplyError(f"Can't find id of user with email '{email}': Unexpected reply {resp.status_code}") diff --git a/nifi.py b/nifi.py index 1cb446bf377bd137c721cfc1f48933b8983624a2..63c4454cd11e85ce1c1242dcded7c738452fb8cc 100644 --- a/nifi.py +++ b/nifi.py @@ -6,7 +6,7 @@ import re from operator import itemgetter import urllib.parse -import config +from config import config # URL to initial login process NIFI_LOGIN_URL = "https://{soctools_proxy}:9443/nifi/login" @@ -38,7 +38,7 @@ def nifi_get_users() -> List[Dict]: :return List of dicts with keys 'id', 'name', 'groups' (list of group names) :raise NifiUnexpectedReplyError """ - url = NIFI_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/tenants/users" + url = NIFI_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/tenants/users" token = _nifi_get_jwt() resp = _send_request('get', url, token) if not resp.ok: @@ -109,14 +109,14 @@ def _nifi_get_jwt() -> str: # We will need to store some cookies - create a session which will automatically handle it # Also, set path to certificates session = requests.Session() - session.verify = config.CA_CERT_FILE - session.cert = (config.MGMT_USER_CERT_PATH, config.MGMT_USER_KEY_PATH) + session.verify = config['ca_cert_file'] + session.cert = (config['mgmt_user_cert_path'], config['mgmt_user_key_path']) # Initiate login process by querying the NiFi login page. # NiFi should set the 'nifi-oidc-request-identifier' cookie (stored into session, will be needed later) and # redirect us to Keycloak. The redirection is automatically followed by requests.get(). # Keycloak should authenticate us using the provided certificate and present a web page with confirmation form. - url = NIFI_LOGIN_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + url = NIFI_LOGIN_URL.format(soctools_proxy=config['soctoolsproxy']) resp = session.get(url, allow_redirects=True) if not resp.ok: raise NifiUnexpectedReplyError(f"_nifi_get_jwt: Received unexpected HTTP status code ({resp.status_code}) from URL '{url}'.") @@ -143,7 +143,7 @@ def _nifi_get_jwt() -> str: # Now, we are authenticated to NiFi, identified by a cookie (stored within our session object). # Use the cookie and ask for the JWT token we need for API requests - url = NIFI_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/access/oidc/exchange" + url = NIFI_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/access/oidc/exchange" resp = session.post(url) # POST must be used even though no data are being sent if not resp.ok: raise NifiUnexpectedReplyError(f"_nifi_get_jwt: Received unexpected HTTP status code ({resp.status_code}) from URL '{url}'.") @@ -154,7 +154,7 @@ def _send_request(method:str, url:str, token:str, data:Optional[dict]=None): return getattr(requests, method)( url, headers={"Authorization": "Bearer " + token}, - verify=config.CA_CERT_FILE, + verify=config['ca_cert_file'], json=data ) @@ -163,7 +163,7 @@ def _get_group_by_name(group_name: str, token=None) -> Optional[dict]: """Return complete user-group specification of a group with given name (or None if not found)""" if not token: token = _nifi_get_jwt() - url = NIFI_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/tenants/user-groups" + url = NIFI_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/tenants/user-groups" resp = _send_request('get', url, token) if not resp.ok: raise NifiUnexpectedReplyError(f"Can't get list of user groups from NiFi: Unexpected reply {resp.status_code}") @@ -200,7 +200,7 @@ def _add_user_to_group(user_id: str, group_name: str, token=None): group["component"]["users"].append({"id": user_id}) # Save new group - url = NIFI_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/tenants/user-groups/" + group_id + url = NIFI_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/tenants/user-groups/" + group_id resp = _send_request('put', url, token, data=group) if not resp.ok: raise NifiUnexpectedReplyError(f"Can't assign user to group in NiFi: Unexpected reply: {resp.status_code} {resp.text[:1000]}") @@ -209,7 +209,7 @@ def _add_user_to_group(user_id: str, group_name: str, token=None): def _add_user(user_name: str, token=None): if not token: token = _nifi_get_jwt() - url = NIFI_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/tenants/users" + url = NIFI_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/tenants/users" post_data = { "revision": {"version": 0}, "component": {"identity": user_name} @@ -227,7 +227,7 @@ def _get_user_by_name(user_name: str, token=None) -> Optional[dict]: """Get user specification of the user with given name (or None if no such user found)""" if not token: token = _nifi_get_jwt() - url = NIFI_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/tenants/search-results?q=" + urllib.parse.quote(user_name) + url = NIFI_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/tenants/search-results?q=" + urllib.parse.quote(user_name) resp = _send_request('get', url, token) if not resp.ok: raise NifiUnexpectedReplyError(f"Can't get user info from NiFi: Unexpected reply: {resp.status_code} {resp.text[:1000]}") @@ -244,7 +244,7 @@ def _delete_user(user_spec: dict, token=None): if not token: token = _nifi_get_jwt() - url = NIFI_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/tenants/users/" + user_spec["id"] + url = NIFI_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/tenants/users/" + user_spec["id"] url += "?version=" + str(int(user_spec["revision"]["version"])) print("DELETE " + url) resp = _send_request('delete', url, token) diff --git a/thehive.py b/thehive.py index 2083b029abade974713df3143bc17a1e8d0c7f7b..2f32d2eb91d1ec3c1e5d7a4efab617c38b6d7cfe 100644 --- a/thehive.py +++ b/thehive.py @@ -4,7 +4,7 @@ from typing import List, Dict, Optional import requests from datetime import datetime -import config +from config import config # Base URL to The Hive API endpoints THEHIVE_API_BASE_URL = "https://{soctools_proxy}:9000/api/v1" @@ -35,10 +35,10 @@ def thehive_get_users() -> List[Dict]: :return List of dicts with keys 'id', 'login', 'name', 'role', 'org', 'created' (datetime) :raise TheHiveUnexpectedReplyError """ - url = THEHIVE_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/query" + url = THEHIVE_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/query" data = { "query": [ - {"_name": "getOrganisation", "idOrName": config.THEHIVE_ORG_NAME}, + {"_name": "getOrganisation", "idOrName": config['thehive_org_name']}, {"_name": "users"} ] } @@ -73,11 +73,11 @@ def thehive_add_user(user: 'UserAccount') -> None: :raise TheHiveUnexpectedReplyError """ - url = THEHIVE_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + "/user" + url = THEHIVE_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + "/user" data = { "login": user.email, "name": f"{user.firstname} {user.lastname}".strip() or user.username, - "organisation": config.THEHIVE_ORG_NAME, + "organisation": config['thehive_org_name'], "profile": "analyst", # TODO allow to set different roles? #"email": user.email, #"password": "", @@ -97,10 +97,10 @@ def thehive_edit_user(login: str, user: 'UserAccount') -> None: :raise TheHiveUnexpectedReplyError """ - url = THEHIVE_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + f"/user/{login}" + url = THEHIVE_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + f"/user/{login}" data = { "name": f"{user.firstname} {user.lastname}".strip() or user.username, - #"organisation": config.THEHIVE_ORG_NAME, + #"organisation": config['thehive_org_name'], #"profile": "analyst", # TODO allow to set different roles? } @@ -119,7 +119,7 @@ def thehive_delete_user(login: str) -> None: :raise TheHiveUnexpectedReplyError """ - url = THEHIVE_API_BASE_URL.format(soctools_proxy=config.SOCTOOLSPROXY) + f"/user/{login}/force?organisation={config.THEHIVE_ORG_NAME}" + url = THEHIVE_API_BASE_URL.format(soctools_proxy=config['soctoolsproxy']) + f"/user/{login}/force?organisation={config['thehive_org_name']}" #print(url) resp = _send_request("delete", url) #print(resp.text) @@ -138,8 +138,8 @@ def _send_request(method:str, url:str, data:Optional[dict]=None): return getattr(requests, method)( url, headers={ - "Authorization": "Bearer " + config.THEHIVE_API_KEY, + "Authorization": "Bearer " + config['thehive_api_key'], }, - verify=config.CA_CERT_FILE, + verify=config['ca_cert_file'], json=data )