├── .gitignore ├── GroupHound.py ├── LICENSE ├── README.md ├── SessionHound.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /GroupHound.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse, csv, getpass, logging, os, sys, warnings 4 | 5 | import neo4j.exceptions 6 | from neo4j import GraphDatabase 7 | from timeit import default_timer as timer 8 | 9 | 10 | class BloodHoundDatabase(object): 11 | def __init__(self, connection_string="bolt://localhost:7687", username="neo4j", password="neo4j"): 12 | # Default Database Values 13 | self.neo4jDB = connection_string 14 | self.username = username 15 | self.password = password 16 | self.driver = None 17 | self.logger = logging.getLogger('GroupHound') 18 | 19 | def connect_database(self): 20 | # Close the database if it is connected 21 | if self.driver is not None: 22 | self.driver.close() 23 | 24 | try: 25 | self.driver = GraphDatabase.driver(self.neo4jDB, auth=(self.username, self.password)) 26 | self.driver.verify_connectivity() 27 | self.logger.info('[+] Successfully connected.') 28 | return True 29 | except (neo4j.exceptions.AuthError, neo4j.exceptions.ServiceUnavailable) as e: 30 | self.logger.info('[-] Error connecting to neo4j: ') 31 | self.logger.error(e.message) 32 | self.driver.close() 33 | return False 34 | 35 | def run_query(self, query, parameters=None, relation=None, query_type=None): 36 | try: 37 | session = self.driver.session() 38 | start = timer() 39 | if parameters['type']: 40 | query = query.format(lType=parameters['type'], relation=relation) 41 | results = session.run(query, parameters=parameters) 42 | self.logger.debug(f"[+] {query} with userName = {parameters['userName']} and " 43 | f"hostName = {parameters['hostName']} " 44 | f"ran in {timer() - start}s") 45 | except Exception as e: 46 | session.close() 47 | self.logger.error('[-] Neo4j query failed to execute.') 48 | self.logger.error(e) 49 | raise SystemExit 50 | if query_type == 'int': 51 | for result in results: 52 | session.close() 53 | return result[0] 54 | elif query_type == 'list': 55 | result_list = [] 56 | keys = results.keys() 57 | for result in results: 58 | full_return = '' 59 | total_keys = len(keys) 60 | for key in range(0, total_keys): 61 | full_return += f'{keys[key]}: {result[key]}' 62 | if (key + 1) < total_keys: 63 | self.logger.debug(f'[+] key: {key}, total_keys: {total_keys}') 64 | full_return += ', ' 65 | result_list.append(full_return) 66 | session.close() 67 | return result_list 68 | else: 69 | result_list = [] 70 | keys = results.keys() 71 | for result in results: 72 | result_list.append(result[keys[0]]) 73 | session.close() 74 | return result_list 75 | 76 | 77 | def get_csv_data(csv_path): 78 | session_list = [] 79 | with open(csv_path, 'r') as csvFile: 80 | header = next(csv.reader(csvFile)) 81 | if not header == ['username', 'hostname', 'type']: 82 | return None 83 | with open(csv_path, 'r') as csvFile: 84 | csv_reader = csv.DictReader(csvFile) 85 | try: 86 | for row in csv_reader: 87 | session_list.append({'userName': row['username'].upper(), 'hostName': row['hostname'].upper(), 88 | 'type': row['type'].capitalize()}) 89 | except Exception as e: 90 | logger.error('[-] Exception while reading CSV data: ') 91 | logger.error(row) 92 | logger.error(e) 93 | return session_list 94 | 95 | 96 | def is_valid_file(parser, arg): 97 | if not os.path.exists(arg): 98 | parser.error(f"[-] The file {arg} does not exist!") 99 | else: 100 | return arg 101 | 102 | 103 | def main(csv_data, relation_type, connection_string="bolt://localhost:7687", username="neo4j", password="neo4j", 104 | dry_run=False): 105 | # Create a BloodHound Neo4j Database Object 106 | bh_database = BloodHoundDatabase(connection_string=connection_string, username=username, password=password) 107 | 108 | # Connect to the Neo4j Database 109 | logger.info('[+] Connecting to Neo4j Database...') 110 | if not bh_database.connect_database(): 111 | logger.info("[-] Unable to connect to neo4j database") 112 | return False 113 | 114 | # Set the relationship type 115 | if relation_type == 'adminto': 116 | relation = "AdminTo" 117 | elif relation_type == 'canrdp': 118 | relation = "CanRDP" 119 | elif relation_type == 'canpsremote': 120 | relation = "CanPSRemote" 121 | elif relation_type == 'executedcom': 122 | relation = "ExecuteDCOM" 123 | 124 | exists_query = """MATCH (c:Computer), (u:{lType}), p=(u)-[r:{relation}]->(c) 125 | WHERE c.name = $hostName AND u.name = $userName 126 | RETURN COUNT(p)""" 127 | add_query = """MATCH (c:Computer), (u:{lType}) 128 | WHERE c.name = $hostName AND u.name = $userName 129 | CREATE (u)-[r:{relation}]->(c) 130 | RETURN type(r)""" 131 | 132 | exists_query_sid = """MATCH (c:Computer), (u:{lType}), p=(u)-[r:{relation}]->(c) 133 | WHERE c.name = $hostName AND u.objectid = $userName 134 | RETURN COUNT(p)""" 135 | add_query_sid = """MATCH (c:Computer), (u:{lType}) 136 | WHERE c.name = $hostName AND u.objectid = $userName 137 | CREATE (u)-[r:{relation}]->(c) 138 | RETURN type(r)""" 139 | if not dry_run: 140 | # Import the data 141 | logger.info('[+] Importing data from CSV file...') 142 | for user in csv_data: 143 | logger.debug(user) 144 | if user['userName'].upper().startswith('S-1-5-'): 145 | user['userName'] = user['userName'].split('@')[0] 146 | if bh_database.run_query(exists_query_sid, user, relation, 'int') < 1: 147 | if relation in bh_database.run_query(add_query_sid, user, relation): 148 | logger.info(f"[-] Successfully added {relation} relation for {user['userName']} " 149 | f"to {user['hostName']}.") 150 | else: 151 | logger.info(f"[-] Failed to add {relation} relation for {user['userName']} to " 152 | f"{user['hostName']}. Verify that username and hostname exist and try running " 153 | f"again.") 154 | else: 155 | # Try to see if it's a group 156 | user['type'] = "Group" 157 | if bh_database.run_query(exists_query_sid, user, relation, 'int') < 1: 158 | if relation in bh_database.run_query(add_query_sid, user, relation): 159 | logger.info(f"[+] Successfully added {relation} relation " 160 | f"for {user['userName']} to {user['hostName']}.") 161 | else: 162 | logger.info(f"[-] Failed to add {relation} relation for {user['userName']} " 163 | f"to {user['hostName']}. Verify that username and hostname exist and try " 164 | f"running again.") 165 | else: 166 | logger.info(f"[-] {relation} relation already exists for userName = {user['userName']} " 167 | f"on hostName = {user['hostName']}, skiping.") 168 | else: 169 | if bh_database.run_query(exists_query, user, relation, 'int') < 1: 170 | if relation in bh_database.run_query(add_query, user, relation): 171 | logger.info(f"[+] Successfully added {relation} relation for {user['userName']} " 172 | f"to {user['hostName']}.") 173 | else: 174 | logger.info(f"[-] Failed to add {relation} relation " 175 | f"for {user['userName']} to {user['hostName']}. Verify that username and hostname " 176 | f"exist and try running again.") 177 | else: 178 | logger.info(f"[-] {relation} relation already exists for userName = {user['userName']} " 179 | f"on hostName = {user['hostName']}, skiping.""") 180 | 181 | else: 182 | logger.info('[+] No further action taken, as this is a dry-run.') 183 | 184 | 185 | if __name__ == "__main__": 186 | 187 | # Parse the command line arguments 188 | parser = argparse.ArgumentParser(description='Import computer local group data from a CSV file into ' 189 | 'BloodHound\'s Neo4j database.\n\nThe CSV should have three colums ' 190 | 'matching the following header structure:' 191 | '\n\n[\'username\', \'hostname\', \'type\']\n\n') 192 | 193 | parser.add_argument('csv', type=lambda x: is_valid_file(parser, x), help='The path to the CSV file containing' 194 | ' the session data to import.') 195 | parser.add_argument('type', type=str.lower, choices=['adminto', 'canrdp', 'canpsremote', 'executedcom'], 196 | help='The access type: AdminTo, CanRDP, CanPSRemote, or ExecuteDCOM.') 197 | parser.add_argument('--neo4j-uri', default='bolt://localhost:7687', 198 | help='Neo4j connection string (Default: bolt://localhost:7687 )') 199 | parser.add_argument('-u', '--username', default='neo4j', help='Neo4j username (Default: neo4j)') 200 | parser.add_argument('--password', help='Neo4j password. If not provided on the command line, ' 201 | 'you will be prompted to enter it.') 202 | parser.add_argument('--debug', action='store_true', help='Print debug information.') 203 | parser.add_argument('--dry-run', action='store_true', default=False, 204 | help='Verify connectivity to neo4j and check for ' 205 | 'CSV parsing issues, but don\'t actually import data') 206 | 207 | args = parser.parse_args() 208 | 209 | if args.password is not None: 210 | neo4j_password = args.password 211 | else: 212 | neo4j_password = getpass.getpass(prompt='Neo4j Connection Password: ') 213 | 214 | # Setup logging 215 | if args.debug: 216 | logging_level = logging.DEBUG 217 | else: 218 | logging_level = logging.INFO 219 | 220 | logging.basicConfig(level=logging_level, format='%(asctime)s - %(name)s - %(levelname)s: %(message)s') 221 | logger = logging.getLogger('GroupHound') 222 | logger.debug('Debugging logging is on.') 223 | 224 | # Filter out warnings from neo4j's verify_connectivity 225 | warnings.filterwarnings('ignore', "The configuration may change in the future.") 226 | 227 | csv_data = get_csv_data(args.csv) 228 | if csv_data: 229 | main(csv_data, args.type, username=args.username, password=neo4j_password, 230 | connection_string=args.neo4j_uri, dry_run=args.dry_run) 231 | else: 232 | logger.error('[-] Please check the format of your CSV file and ensure it has the expected structure.') 233 | parser.print_help(sys.stderr) 234 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Conor Richard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SessionHound & GroupHound 2 | A pair of scripts to import session and local group information that has been collected from alternate data sources into BloodHound's Neo4j database. 3 | 4 | ## Problem 5 | SharpHound's privileged session collection requires an account with elevated permissions to operate. When using BloodHound as a Blue tool to locate and resolve misconfigurations and identify dangerous behaviors, detailed and accurate session information is highly beneficial. An account that has local administrative rights on all endpoints is a security risk. 6 | 7 | ## Solution 8 | Session information can be obtained from alternate sources. This information can be obtained by collecting the information from centrally logged local Windows Security Events or from other tools that can poll information about logged on users from live endpoints. It can be collected into a spreadsheet and added to the BloodHound database via Cypher queries. 9 | 10 | ## Setup 11 | ``` 12 | git clone https://github.com/xenoscr/SessionHound.git SessionHound 13 | cd SessionHound 14 | pip install -r requirements.txt 15 | ``` 16 | 17 | ## SessionHound Usage 18 | ``` 19 | usage: SessionHound.py [-h] [--neo4j-uri NEO4J_URI] [-u USERNAME] 20 | [--password PASSWORD] [--debug] [--dry-run] 21 | csv 22 | 23 | Import computer session data from a CSV file into BloodHound's Neo4j database. 24 | The CSV should have two colums matching the following header structure: 25 | ['username', 'hostname'] 26 | 27 | positional arguments: 28 | csv The path to the CSV file containing the session data 29 | to import. 30 | 31 | optional arguments: 32 | -h, --help show this help message and exit 33 | --neo4j-uri NEO4J_URI 34 | Neo4j connection string (Default: 35 | bolt://localhost:7687 ) 36 | -u USERNAME, --username USERNAME 37 | Neo4j username (Default: neo4j) 38 | --password PASSWORD Neo4j password. If not provided on the command line, 39 | you will be prompted to enter it. 40 | --debug Print debug information. 41 | --dry-run Verify connectivity to neo4j and check for CSV parsing 42 | issues, but don't actually import data 43 | ``` 44 | 45 | ## SessionHound CSV File Format 46 | The CSV file needs to have two columns: 47 | - username: The User Principal Name (UPN). i.e. USER01@EXAMPLE.COM 48 | - hostname: The Host's FQDN. i.e. HOSTNAME.EXAMPLE.COM 49 | 50 | ### SessionHound CSV Example 51 | ``` 52 | username,hostname 53 | user01@example.com,host01.example.com 54 | user02@example.com,host01.example.com 55 | user02@example.com,host02.example.com 56 | ``` 57 | 58 | ## GroupHound Usage 59 | ``` 60 | usage: GroupHound.py [-h] [--neo4j-uri NEO4J_URI] [-u USERNAME] 61 | [--password PASSWORD] [--debug] [--dry-run] 62 | csv {adminto,canrdp,canpsremote,executedcom} 63 | 64 | Import computer local group data from a CSV file into BloodHound's Neo4j 65 | database. The CSV should have three colums matching the following header 66 | structure: ['username', 'hostname', 'type'] 67 | 68 | positional arguments: 69 | csv The path to the CSV file containing the session data 70 | to import. 71 | {adminto,canrdp,canpsremote,executedcom} 72 | The access type: AdminTo, CanRDP, CanPSRemote, or 73 | ExecuteDCOM. 74 | 75 | optional arguments: 76 | -h, --help show this help message and exit 77 | --neo4j-uri NEO4J_URI 78 | Neo4j connection string (Default: 79 | bolt://localhost:7687 ) 80 | -u USERNAME, --username USERNAME 81 | Neo4j username (Default: neo4j) 82 | --password PASSWORD Neo4j password. If not provided on the command line, 83 | you will be prompted to enter it. 84 | --debug Print debug information. 85 | --dry-run Verify connectivity to neo4j and check for CSV parsing 86 | issues, but don't actually import data 87 | ``` 88 | 89 | ## GroupHound CSV File Format 90 | The CSV file needs to have three columns: 91 | - username: The User Principal Name (UPN). i.e. USER01@EXAMPLE.COM 92 | - hostname: The Host's FQDN. i.e. HOSTNAME.EXAMPLE.COM 93 | - type: The object type. Group or User 94 | 95 | ### Groupound CSV Example 96 | ``` 97 | username,hostname,type 98 | user01@example.com,host01.example.com,User 99 | user02@example.com,host01.example.com,User 100 | group01@example.com,host02.example.com,Group 101 | group02@example.com,host02.example.com,Group 102 | ``` 103 | 104 | **NOTE:** If using Excel to prepare your CSV, saving the CSV in Unicode/UTF-8 format will cause some errors. To avoid these issues use the **CSV (Comma delimited)** option and not **CSV UTF-8 (Comma delimited)**. 105 | -------------------------------------------------------------------------------- /SessionHound.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse, csv, getpass, logging, os, sys, warnings 4 | 5 | import neo4j.exceptions 6 | from neo4j import GraphDatabase 7 | from timeit import default_timer as timer 8 | 9 | 10 | class BloodHoundDatabase(object): 11 | def __init__(self, connection_string="bolt://localhost:7687", username="neo4j", password="neo4j"): 12 | # Default Database Values 13 | self.neo4jDB = connection_string 14 | self.username = username 15 | self.password = password 16 | self.driver = None 17 | self.db_validated = False 18 | self.logger = logging.getLogger('SessionHound') 19 | 20 | def connect_database(self): 21 | # Close the database if it is connected 22 | if self.driver is not None: 23 | self.driver.close() 24 | 25 | try: 26 | self.driver = GraphDatabase.driver(self.neo4jDB, auth=(self.username, self.password)) 27 | self.driver.verify_connectivity() 28 | self.logger.info('[+] Successfully connected.') 29 | return True 30 | except (neo4j.exceptions.AuthError, neo4j.exceptions.ServiceUnavailable) as e: 31 | self.logger.info('[-] Error connecting to neo4j: ') 32 | self.driver.close() 33 | self.logger.error(e.message) 34 | return False 35 | 36 | def session_exists(self, parameters): 37 | query = """MATCH (c:Computer), (u:User), p=(c)-[r:HasSession]->(u) 38 | WHERE c.name = $hostName AND u.name = $userName 39 | RETURN COUNT(p)""" 40 | 41 | try: 42 | session = self.driver.session() 43 | start = timer() 44 | results = session.run(query, parameters=parameters) 45 | 46 | self.logger.debug(f"[+] {query} with userName = {parameters['userName']} and " 47 | f"hostName = {parameters['hostName']} ran in {timer() - start}s") 48 | result = results.single() 49 | session.close() 50 | 51 | return result is None or result[0] != 0 52 | 53 | except Exception as e: 54 | session.close() 55 | self.logger.error('[-] Neo4j query failed to execute.') 56 | self.logger.error(e) 57 | raise SystemExit 58 | 59 | def add_session(self, parameters): 60 | query = """MATCH (c:Computer), (u:User) 61 | WHERE c.name = $hostName AND u.name = $userName 62 | CREATE (c)-[r:HasSession]->(u) 63 | RETURN type(r)""" 64 | 65 | try: 66 | session = self.driver.session() 67 | start = timer() 68 | results = session.run(query, parameters=parameters) 69 | logger.debug(f"[+] {query} with userName = {parameters['userName']} and " 70 | f"hostName = {parameters['hostName']} ran in {timer() - start}s") 71 | 72 | result_list = [] 73 | keys = results.keys() 74 | for result in results: 75 | result_list.append(result[keys[0]]) 76 | session.close() 77 | return result_list 78 | 79 | except Exception as e: 80 | session.close() 81 | self.logger.error('[-] Neo4j query failed.') 82 | self.logger.error(e) 83 | raise SystemExit 84 | 85 | 86 | def get_csv_data(csv_path): 87 | session_list = [] 88 | with open(csv_path, 'r') as csvFile: 89 | header = next(csv.reader(csvFile)) 90 | if not header == ['username', 'hostname']: 91 | logger.error("[-] Error in CSV file. Please ensure that your " 92 | "CSV uses the headers 'username' and 'hostname'.") 93 | return None 94 | with open(csv_path, 'r') as csvFile: 95 | csv_reader = csv.DictReader(csvFile) 96 | try: 97 | for row in csv_reader: 98 | session_list.append({'userName': row['username'].upper(), 'hostName': row['hostname'].upper()}) 99 | except Exception as e: 100 | print(row) 101 | print(e) 102 | return session_list 103 | 104 | 105 | def is_valid_file(parser, arg): 106 | if not os.path.exists(arg): 107 | parser.error("The file %s does not exist!" % arg) 108 | else: 109 | return arg 110 | 111 | 112 | def main(csv_data, connection_string="bolt://localhost:7687", username="neo4j", password="neo4j", dry_run=False): 113 | # Create a BloodHound Neo4j Database Object 114 | bh_database = BloodHoundDatabase(connection_string=connection_string, username=username, password=password) 115 | 116 | # Connect to the Neo4j Database 117 | logger.info('[+] Connecting to Neo4j Database...') 118 | if not bh_database.connect_database(): 119 | logger.info("[-] Unable to connect to neo4j database") 120 | return False 121 | 122 | if not dry_run: 123 | # Import the data 124 | logger.info('[+] Importing data from CSV file...') 125 | for user in csv_data: 126 | logger.debug(f"[+] Importing: {user}") 127 | 128 | if not bh_database.session_exists(user): 129 | if 'HasSession' in bh_database.add_session(user): 130 | logger.info( 131 | f"[+] Successfully added session for {user['userName']} to {user['hostName']}.") 132 | else: 133 | logger.info(f"[-] Failed to add session for {user['userName']} to {user['hostName']}.") 134 | else: 135 | logger.info( 136 | f"[+] Session information already exists for userName = " 137 | f"{user['userName']} on hostName = {user['hostName']}, skipping.") 138 | else: 139 | logger.info('[+] No further action taken, as this is a dry-run.') 140 | 141 | 142 | if __name__ == "__main__": 143 | # Parse the command line arguments 144 | parser = argparse.ArgumentParser( 145 | description='Import computer session data from a CSV file into BloodHound\'s Neo4j database.\n\n' 146 | 'The CSV should have two columns matching the following header ' 147 | 'structure:\n\n[\'username\', \'hostname\']\n\n') 148 | parser.add_argument('csv', type=lambda x: is_valid_file(parser, x), help='The path to the CSV file containing ' 149 | 'the session data to import.') 150 | parser.add_argument('--neo4j-uri', default='bolt://localhost:7687', 151 | help='Neo4j connection string (Default: bolt://localhost:7687 )') 152 | parser.add_argument('-u', '--username', default='neo4j', help='Neo4j username (Default: neo4j)') 153 | parser.add_argument('--password', help='Neo4j password. If not provided on the command line, ' 154 | 'you will be prompted to enter it.') 155 | parser.add_argument('--debug', action='store_true', help='Print debug information.') 156 | parser.add_argument('--dry-run', action='store_true', default=False, 157 | help='Verify connectivity to neo4j and check for ' 158 | 'CSV parsing issues, but don\'t actually import data') 159 | args = parser.parse_args() 160 | 161 | if args.password is not None: 162 | neo4j_password = args.password 163 | else: 164 | neo4j_password = getpass.getpass(prompt='Neo4j Connection Password: ') 165 | 166 | # Setup logging 167 | if args.debug: 168 | logging_level = logging.DEBUG 169 | else: 170 | logging_level = logging.INFO 171 | 172 | logging.basicConfig(level=logging_level, format='%(asctime)s - %(name)s - %(levelname)s: %(message)s') 173 | logger = logging.getLogger('SessionHound') 174 | logger.debug('Debugging logging is on.') 175 | 176 | # Filter out warnings from neo4j's verify_connectivity 177 | warnings.filterwarnings('ignore', "The configuration may change in the future.") 178 | 179 | csv_data = get_csv_data(args.csv) 180 | if csv_data: 181 | main(csv_data, username=args.username, password=neo4j_password, 182 | connection_string=args.neo4j_uri, dry_run=args.dry_run) 183 | else: 184 | logger.error('[-] Please check the format of your CSV file and ensure it has the expected structure.\n') 185 | parser.print_help(sys.stderr) 186 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenoscr/SessionHound/b7516dd3c60e66e6cc2f0c43f3a5cd214aafde0f/requirements.txt --------------------------------------------------------------------------------