Codebase list python-faraday / 62d1b14 faraday / server / commands / initdb.py
62d1b14

Tree @62d1b14 (Download .tar.gz)

initdb.py @62d1b14raw · history · blame

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
"""
Faraday Penetration Test IDE
Copyright (C) 2013  Infobyte LLC (http://www.infobytesec.com/)
See the file 'doc/LICENSE' for the license information

"""

import getpass
import string
import uuid
import os
import sys
import click
import psycopg2
from alembic.config import Config
from alembic import command
from random import SystemRandom
from tempfile import TemporaryFile
from subprocess import Popen  # nosec

import sqlalchemy
from sqlalchemy import create_engine
from sqlalchemy.sql import text

from faraday.server.utils.database import is_unique_constraint_violation

from configparser import ConfigParser, NoSectionError

from flask import current_app
from flask_security.utils import hash_password

from colorama import init
from colorama import Fore
from sqlalchemy.exc import OperationalError, ProgrammingError

import faraday.server.config
from faraday.server.config import CONST_FARADAY_HOME_PATH
from faraday.server.config import LOCAL_CONFIG_FILE, FARADAY_BASE

init()


class InitDB():

    def _check_current_config(self, config):
        try:
            config.get('database', 'connection_string')
            reconfigure = None
            while not reconfigure:
                reconfigure = input(
                    f'Database section {Fore.YELLOW} already found{Fore.WHITE}. Do you want to reconfigure database? (yes/no) ')
                if reconfigure.lower() == 'no':
                    return False
                elif reconfigure.lower() == 'yes':
                    continue
                else:
                    reconfigure = None
        except NoSectionError:
            config.add_section('database')

        return True

    def run(self, choose_password, faraday_user_password):
        """
             Main entry point that executes these steps:
                 * creates role in database.
                 * creates database.
                 * save new configuration on server.ini.
                 * creates tables.
        """
        try:
            config = ConfigParser()
            config.read(LOCAL_CONFIG_FILE)
            if not self._check_current_config(config):
                return
            faraday_path_conf = CONST_FARADAY_HOME_PATH
            # we use psql_log_filename for historical saving. we will ask faraday users this file.
            # current_psql_output is for checking psql command already known errors for each execution.
            psql_log_filename = faraday_path_conf / 'logs' / 'psql_log.log'
            current_psql_output = TemporaryFile()
            with open(psql_log_filename, 'ab+') as psql_log_file:
                hostname = 'localhost'
                username, password, process_status = self._configure_new_postgres_user(current_psql_output)
                current_psql_output.seek(0)
                psql_output = current_psql_output.read()
                # persist log in the faraday log psql_log.log
                psql_log_file.write(psql_output)
                self._check_psql_output(current_psql_output, process_status)

                if hostname.lower() in ['localhost', '127.0.0.1']:
                    database_name = os.environ.get("FARADAY_DATABASE_NAME", "faraday")
                    current_psql_output = TemporaryFile()
                    database_name, process_status = self._create_database(database_name, username, current_psql_output)
                    current_psql_output.seek(0)
                    self._check_psql_output(current_psql_output, process_status)

            current_psql_output.close()
            conn_string = self._save_config(config, username, password, database_name, hostname)
            self._create_tables(conn_string)
            self._create_admin_user(conn_string, choose_password, faraday_user_password)
        except KeyboardInterrupt:
            current_psql_output.close()
            print('User cancelled.')
            sys.exit(1)

    def _create_roles(self, conn_string):
        engine = create_engine(conn_string)
        try:
            statement = text(
                "INSERT INTO faraday_role(name) VALUES ('admin'),('pentester'),('client'),('asset_owner');"
            )
            connection = engine.connect()
            connection.execute(statement)
        except sqlalchemy.exc.IntegrityError as ex:
            if is_unique_constraint_violation(ex):
                # when re using database user could be created previously
                print(
                    "{yellow}WARNING{white}: Faraday administrator user already exists.".format(
                        yellow=Fore.YELLOW, white=Fore.WHITE))
            else:
                print(
                    "{yellow}WARNING{white}: Can't create administrator user.".format(
                        yellow=Fore.YELLOW, white=Fore.WHITE))
                raise

    def _create_initial_notifications_config(self):
        from faraday.server.models import (db,  # pylint:disable=import-outside-toplevel
                                           Role,  # pylint:disable=import-outside-toplevel
                                           NotificationSubscription,  # pylint:disable=import-outside-toplevel
                                           NotificationSubscriptionWebSocketConfig,  # pylint:disable=import-outside-toplevel
                                           EventType,  # pylint:disable=import-outside-toplevel
                                           User,  # pylint:disable=import-outside-toplevel
                                           ObjectType)  # pylint:disable=import-outside-toplevel

        admin = User.ADMIN_ROLE
        pentester = User.PENTESTER_ROLE
        asset_owner = User.ASSET_OWNER_ROLE
        client = User.CLIENT_ROLE

        default_initial_notifications_config = [
            # Workspace
            {'roles': [admin], 'event_types': ['new_workspace', 'update_workspace', 'delete_workspace']},
            # Users
            {'roles': [admin], 'event_types': ['new_user', 'update_user', 'delete_user']},
            # Agents
            {'roles': [admin, pentester], 'event_types': ['new_agent', 'update_agent', 'delete_agent']},
            # Reports
            {'roles': [admin, pentester, asset_owner],
             'event_types': ['new_executivereport', 'update_executivereport', 'delete_executivereport']},
            # Agent execution
            {'roles': [admin, pentester, asset_owner], 'event_types': ['new_agentexecution']},
            # Commands
            {'roles': [admin, pentester, asset_owner], 'event_types': ['new_command']},
            # Vulnerability
            {'roles': [admin, pentester, asset_owner, client],
             'event_types': ['new_vulnerability', 'update_vulnerability', 'delete_vulnerability']},
            # Vulnerability Web
            {'roles': [admin, pentester, asset_owner, client],
             'event_types': ['new_vulnerabilityweb', 'update_vulnerabilityweb', 'delete_vulnerabilityweb']},
            # Comments
            {'roles': [admin, pentester, asset_owner, client], 'event_types': ['new_comment']},
            # Comments
            {'roles': [admin, pentester, asset_owner, client],
             'event_types': ['new_host', 'update_host', 'delete_host']},
        ]

        event_types = [('new_workspace', False),
                       ('new_agent', True),
                       ('new_user', False),
                       ('new_agentexecution', True),
                       ('new_executivereport', True),
                       ('new_vulnerability', False),
                       ('new_command', True),
                       ('new_comment', False),
                       ('update_workspace', False),
                       ('update_agent', False),
                       ('update_user', False),
                       ('update_executivereport', True),
                       ('update_vulnerability', False),
                       ('delete_workspace', False),
                       ('delete_agent', False),
                       ('delete_user', False),
                       ('delete_executivereport', False),
                       ('delete_vulnerability', False),
                       ('new_vulnerabilityweb', False),
                       ('update_vulnerabilityweb', False),
                       ('delete_vulnerabilityweb', False),
                       ('new_host', False),
                       ('update_host', False),
                       ('delete_host', False)
                       ]

        default_initial_enabled_notifications_config = ['new_workspace', 'update_executivereport', 'new_agentexecution', 'new_command', 'new_comment']

        for event_type in event_types:
            enabled = False
            if event_type[0] in default_initial_enabled_notifications_config:
                enabled = True
            event_type_obj = EventType(name=event_type[0], async_event=event_type[1], enabled=enabled)
            db.session.add(event_type_obj)

        object_types = ['vulnerability',
                        'vulnerabilityweb',
                        'host',
                        'credential',
                        'service',
                        'source_code',
                        'comment',
                        'executivereport',
                        'workspace',
                        'task',
                        'agent',
                        'agentexecution',
                        'command',
                        'user']

        for object_type in object_types:
            obj = ObjectType(name=object_type)
            db.session.add(obj)
            db.session.commit()

        for config in default_initial_notifications_config:
            for event_type in config['event_types']:
                allowed_roles_objs = Role.query.filter(Role.name.in_(config['roles'])).all()
                event_type_obj = EventType.query.filter(EventType.name == event_type).first()
                n = NotificationSubscription(event_type=event_type_obj, allowed_roles=allowed_roles_objs)
                db.session.add(n)
                db.session.commit()
                active = False
                if event_type in default_initial_enabled_notifications_config:
                    active = True
                ns = NotificationSubscriptionWebSocketConfig(subscription=n, active=active, role_level=True)
                db.session.add(ns)
                db.session.commit()

    def _create_admin_user(self, conn_string, choose_password, faraday_user_password):
        engine = create_engine(conn_string)
        # TODO change the random_password variable name, it is not always
        # random anymore
        if choose_password:
            user_password = click.prompt(
                'Enter the desired password for the "faraday" user',
                confirmation_prompt=True,
                hide_input=True
            )
        else:
            if faraday_user_password:
                user_password = faraday_user_password
            else:
                user_password = self.generate_random_pw(12)
        already_created = False
        fs_uniquifier = str(uuid.uuid4())
        try:

            statement = text("""
                INSERT INTO faraday_user (
                            username, name, password,
                            is_ldap, active, last_login_ip,
                            current_login_ip, state_otp, fs_uniquifier
                        ) VALUES (
                            'faraday', 'Administrator', :password,
                            false, true, '127.0.0.1',
                            '127.0.0.1', 'disabled', :fs_uniquifier
                        )
            """)
            params = {
                'password': hash_password(user_password),
                'fs_uniquifier': fs_uniquifier
            }
            connection = engine.connect()
            connection.execute(statement, **params)
            result = connection.execute(text("""SELECT id, username FROM faraday_user"""))
            user_id = list(user_tuple[0] for user_tuple in result if user_tuple[1] == "faraday")[0]
            result = connection.execute(text("""SELECT id, name FROM faraday_role"""))
            role_id = list(role_tuple[0] for role_tuple in result if role_tuple[1] == "admin")[0]
            params = {
                "user_id": user_id,
                "role_id": role_id
            }
            connection.execute(text("INSERT INTO roles_users(user_id, role_id) VALUES (:user_id, :role_id)"), **params)
        except sqlalchemy.exc.IntegrityError as ex:
            if is_unique_constraint_violation(ex):
                # when re using database user could be created previously
                already_created = True
                print(
                    "{yellow}WARNING{white}: Faraday administrator user already exists.".format(
                        yellow=Fore.YELLOW, white=Fore.WHITE))
            else:
                print(
                    "{yellow}WARNING{white}: Can't create administrator user.".format(
                        yellow=Fore.YELLOW, white=Fore.WHITE))
                raise
        if not already_created:
            print("Admin user created with \n\n{red}username: {white}faraday \n"
                  "{red}password:{white} {"
                  "user_password} \n".format(user_password=user_password,
                                             white=Fore.WHITE, red=Fore.RED))

    def _configure_existing_postgres_user(self):
        username = input('Please enter the postgresql username: ')
        password = getpass.getpass('Please enter the postgresql password: ')

        return username, password

    def _check_psql_output(self, current_psql_output_file, process_status):
        current_psql_output_file.seek(0)
        psql_output = current_psql_output_file.read().decode('utf-8')
        if 'unknown user: postgres' in psql_output:
            print(f'ERROR: Postgres user not found. Did you install package {Fore.BLUE}postgresql{Fore.WHITE}?')
        elif 'could not connect to server' in psql_output:
            print(
                f'ERROR: {Fore.RED}PostgreSQL service{Fore.WHITE} is not running. Please verify that it is running in port 5432 before executing setup script.')
        elif process_status > 0:
            current_psql_output_file.seek(0)
            print('ERROR: ' + psql_output)

        if process_status != 0:
            current_psql_output_file.close()  # delete temp file
            sys.exit(process_status)

    def generate_random_pw(self, pwlen):
        rng = SystemRandom()
        return "".join([rng.choice(string.ascii_letters + string.digits) for _ in range(pwlen)])

    def _configure_new_postgres_user(self, psql_log_file):
        """
            This step will create the role on the database.
            we return username and password and those values will be saved in the config file.
        """
        print(
            'This script will {blue} create a new postgres user {white} and {blue} save faraday-server settings {white}(server.ini). '.format(
                blue=Fore.BLUE, white=Fore.WHITE))
        username = os.environ.get("FARADAY_DATABASE_USER", 'faraday_postgresql')
        postgres_command = ['sudo', '-u', 'postgres', 'psql']
        if sys.platform == 'darwin':
            print(f'{Fore.BLUE}MAC OS detected{Fore.WHITE}')
            postgres_command = ['psql', 'postgres']
        password = self.generate_random_pw(25)
        command = postgres_command + ['-c', f'CREATE ROLE {username} WITH LOGIN PASSWORD \'{password}\';']
        p = Popen(command, stderr=psql_log_file, stdout=psql_log_file)  # nosec
        p.wait()
        psql_log_file.seek(0)
        output = psql_log_file.read()
        if isinstance(output, bytes):
            output = output.decode('utf-8')
        already_exists_error = f'role "{username}" already exists'
        return_code = p.returncode
        if already_exists_error in output:
            print(f"{Fore.YELLOW}WARNING{Fore.WHITE}: Role {username} already exists, skipping creation ")

            try:
                if not getattr(faraday.server.config, 'database', None):
                    print(
                        'Manual configuration? \n faraday_postgresql was found in PostgreSQL, but no connection string was found in server.ini. ')
                    print(
                        'Please configure [database] section with correct postgresql string. Ex. postgresql+psycopg2://faraday_postgresql:PASSWORD@localhost/faraday')
                    sys.exit(1)
                try:
                    password = faraday.server.config.database.connection_string.split(':')[2].split('@')[0]
                except AttributeError:
                    print('Could not find connection string.')
                    print(
                        'Please configure [database] section with correct postgresql string. Ex. postgresql+psycopg2://faraday_postgresql:PASSWORD@localhost/faraday')
                    sys.exit(1)
                connection = psycopg2.connect(dbname='postgres',
                                              user=username,
                                              password=password)
                cur = connection.cursor()
                cur.execute('SELECT * FROM pg_catalog.pg_tables;')
                cur.fetchall()
                connection.commit()
                connection.close()
            except psycopg2.Error as e:
                if 'authentication failed' in str(e):
                    print('{red}ERROR{white}: User {username} already '
                          'exists'.format(white=Fore.WHITE,
                                          red=Fore.RED,
                                          username=username))
                    sys.exit(1)
                else:
                    raise
            return_code = 0
        return username, password, return_code

    def _create_database(self, database_name, username, psql_log_file):
        """
             This step uses the createdb command to add a new database.
        """
        postgres_command = ['sudo', '-u', 'postgres']
        if sys.platform == 'darwin':
            postgres_command = []

        print(f'Creating database {database_name}')
        command = postgres_command + ['createdb', '-E', 'utf8', '-O', username, database_name]
        p = Popen(command, stderr=psql_log_file, stdout=psql_log_file, cwd='/tmp')  # nosec
        p.wait()
        return_code = p.returncode
        psql_log_file.seek(0)
        output = psql_log_file.read().decode('utf-8')
        already_exists_error = f'database creation failed: ERROR:  database "{database_name}" already exists'
        if already_exists_error in output:
            print(f'{Fore.YELLOW}WARNING{Fore.WHITE}: Database already exists.')
            return_code = 0
        return database_name, return_code

    def _save_config(self, config, username, password, database_name, hostname):
        """
             This step saves database configuration to server.ini
        """
        print(f'Saving database credentials file in {LOCAL_CONFIG_FILE}')

        conn_string = 'postgresql+psycopg2://{username}:{password}@{server}/{database_name}'.format(
            username=username,
            password=password,
            server=hostname,
            database_name=database_name
        )
        config.set('database', 'connection_string', conn_string)
        with open(LOCAL_CONFIG_FILE, 'w') as configfile:
            config.write(configfile)
        return conn_string

    def _create_tables(self, conn_string):
        print('Creating tables')
        from faraday.server.models import db  # pylint:disable=import-outside-toplevel
        current_app.config['SQLALCHEMY_DATABASE_URI'] = conn_string

        # Check if the alembic_version exists
        # Taken from https://stackoverflow.com/a/24089729
        (result,) = list(db.session.execute("select to_regclass('alembic_version')"))
        exists = result[0] is not None

        if exists:
            print("Faraday tables already exist in the database. No tables will "
                  "be created. If you want to ugprade the schema to the latest "
                  "version, you should run \"faraday-manage migrate\".")
            return

        try:
            db.create_all()
        except OperationalError as ex:
            if 'could not connect to server' in str(ex):
                print(
                    f'ERROR: {Fore.RED}PostgreSQL service{Fore.WHITE} is not running. Please verify that it is running in port 5432 before executing setup script.')
                sys.exit(1)
            elif 'password authentication failed' in str(ex):
                print('ERROR: ')
                sys.exit(1)
            else:
                raise
        except ProgrammingError as ex:
            print(ex)
            print('Please check postgres user permissions.')
            sys.exit(1)
        except ImportError as ex:
            if 'psycopg2' in str(ex):
                print(
                    f'ERROR: Missing python depency {Fore.RED}psycopg2{Fore.WHITE}. Please install it with {Fore.BLUE}pip install psycopg2')
                sys.exit(1)
            else:
                raise
        else:
            alembic_cfg = Config(FARADAY_BASE / 'alembic.ini')
            os.chdir(FARADAY_BASE)
            command.stamp(alembic_cfg, "head")
            # TODO ADD RETURN TO PREV DIR
        self._create_roles(conn_string)
        self._create_initial_notifications_config()