├── .gitignore ├── LICENSE ├── README.md ├── histstat ├── __init__.py └── histstat.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | dist/ 4 | build/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Austin Jackson 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 | # histstat 2 | 3 | This is a cross-platform command-line tool for obtaining live, rudimentary network connection data on a computer system. This tool was designed for network and security analysts to easily view connections on a system **as they occur**. It will display useful information about network connections that utilities like netstat typically won't give you such as what time the connection was made, the exact command that created the connection, and the user that connection was made by. 4 | 5 | **Note for Windows users:** Detailed process information will not display unless you're running as `NT AUTHORITY\SYSTEM`. An easy way to drop into a system-level command prompt is to use PsExec from [SysInternals](https://technet.microsoft.com/en-us/sysinternals/bb842062.aspx). Run `psexec -i -s cmd.exe` as Administrator and then run histstat. 6 | 7 | ### Install 8 | 9 | *nix/macOS: 10 | ``` 11 | sudo pip install histstat 12 | ``` 13 | 14 | Windows (open cmd.exe as Administrator): 15 | ``` 16 | python -m pip install histstat 17 | ``` 18 | 19 | ### Example Usage 20 | 21 | ``` 22 | $ histstat --help 23 | usage: histstat [-h] [-i INTERVAL] [-j] [-l LOG] [-p] [-q] [-v] [--hash] 24 | 25 | history for netstat 26 | 27 | optional arguments: 28 | -h, --help show this help message and exit 29 | -i INTERVAL, --interval INTERVAL 30 | specify update interval in seconds 31 | -j, --json json output 32 | -l LOG, --log LOG log output to a file 33 | -p, --prettify prettify output 34 | -q, --quiet quiet mode, do not output to stdout (for use when logging) 35 | -v, --version display the current version 36 | --hash takes md5 and sha256 hashes of process files (warning: slow!) 37 | 38 | $ sudo histstat -p -l log.txt 39 | date time proto laddr lport raddr rport status user pid pname command 40 | 19-06-18 21:18:44 tcp 0.0.0.0 22 * * LISTEN root 650 sshd /usr/bin/sshd -D 41 | 19-06-18 21:18:44 udp 0.0.0.0 68 * * - root 647 dhcpcd /usr/bin/dhcpcd -q -b 42 | 19-06-18 21:18:51 tcp 0.0.0.0 8000 * * LISTEN vesche 5435 python python -m http.server 43 | 19-06-18 21:19:11 tcp 0.0.0.0 1337 * * LISTEN vesche 5602 ncat ncat -l -p 1337 44 | 19-06-18 21:19:26 tcp 127.0.0.1 39246 * * LISTEN vesche 5772 electron /usr/lib/electron/electron --nolazy --inspect=39246 /usr/lib/code/out/bootstrap-fork --type=extensionHost 45 | 19-06-18 21:19:28 tcp 10.13.37.114 43924 13.107.6.175 443 ESTABLISHED vesche 5689 code-oss /usr/lib/electron/electron /usr/lib/code/code.js 46 | ... 47 | ``` 48 | 49 | ### Thanks 50 | 51 | Huge thanks to Giampaolo Rodola' (giampaolo) and all the contributers of [psutil](https://github.com/giampaolo/psutil) for the amazing open source library that this project relies upon completely. 52 | 53 | Also, thanks to gleitz and his project [howdoi](https://github.com/gleitz/howdoi), in my refactor of histstat I modeled my code around his command line tool as the code is exceptionally clean and readable. 54 | 55 | A big thanks to JavaScriptDude who has a [fantastic fork of histstat](https://github.com/JavaScriptDude/histstat) with many additional features, some of which have now been implemented in this project such as: optional IP geolocation and quiet mode for logging. -------------------------------------------------------------------------------- /histstat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesche/histstat/d541a99ea147702875f965b025d2338398f305f6/histstat/__init__.py -------------------------------------------------------------------------------- /histstat/histstat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | histstat, history for netstat 5 | https://github.com/vesche/histstat 6 | """ 7 | 8 | import os 9 | import sys 10 | import time 11 | import psutil 12 | import hashlib 13 | import argparse 14 | import datetime 15 | 16 | from socket import AF_INET, AF_INET6, SOCK_DGRAM, SOCK_STREAM 17 | 18 | __version__ = '1.2.0' 19 | 20 | PROTOCOLS = { 21 | (AF_INET, SOCK_STREAM): 'tcp', 22 | (AF_INET6, SOCK_STREAM): 'tcp6', 23 | (AF_INET, SOCK_DGRAM): 'udp', 24 | (AF_INET6, SOCK_DGRAM): 'udp6' 25 | } 26 | FIELDS = [ 27 | 'date', 'time', 'proto', 'laddr', 'lport', 'raddr', 'rport', 'status', 28 | 'user', 'pid', 'pname', 'command' 29 | ] 30 | P_FIELDS = '{:<8} {:<8} {:<5} {:<15.15} {:<5} {:<15.15} {:<5} {:<11} ' \ 31 | '{:<20.20} {:<5} {:<20.20} {}' 32 | BUF_SIZE = 65536 33 | 34 | if sys.platform.startswith('linux') or sys.platform == 'darwin': 35 | PLATFORM = 'nix' 36 | from os import geteuid 37 | elif sys.platform.startswith('win'): 38 | PLATFORM = 'win' 39 | from ctypes import * 40 | else: 41 | print('Error: Platform unsupported.') 42 | sys.exit(1) 43 | 44 | 45 | def histmain(interval): 46 | """Primary execution function for histstat.""" 47 | 48 | # ladies and gentlemen this is your captain speaking 49 | output.preflight() 50 | 51 | # get initial connections 52 | connections_A = psutil.net_connections() 53 | for c in connections_A: 54 | output.process(process_conn(c)) 55 | 56 | # primary loop 57 | while True: 58 | time.sleep(interval) 59 | connections_B = psutil.net_connections() 60 | for c in connections_B: 61 | if c not in connections_A: 62 | output.process(process_conn(c)) 63 | connections_A = connections_B 64 | 65 | 66 | def process_conn(c): 67 | """Process a psutil._common.sconn object into a list of raw data.""" 68 | 69 | date, time = str(datetime.datetime.now()).split() 70 | proto = PROTOCOLS[(c.family, c.type)] 71 | raddr = rport = '*' 72 | status = pid = pname = user = command = '-' 73 | laddr, lport = c.laddr 74 | 75 | if c.raddr: 76 | raddr, rport = c.raddr 77 | if c.pid: 78 | try: 79 | pname, pid = psutil.Process(c.pid).name(), str(c.pid) 80 | user = psutil.Process(c.pid).username() 81 | command = ' '.join(psutil.Process(c.pid).cmdline()) 82 | except: 83 | pass # if process closes during processing 84 | if c.status != 'NONE': 85 | status = c.status 86 | 87 | return [ 88 | date[2:], time[:8], proto, laddr, lport, raddr, rport, status, 89 | user, pid, pname, command 90 | ] 91 | 92 | 93 | def hash_file(path): 94 | """Hashes a file using MD5 and SHA256.""" 95 | 96 | md5_hash = hashlib.md5() 97 | sha256_hash = hashlib.sha256() 98 | with open(path, 'rb') as f: 99 | while True: 100 | data = f.read(BUF_SIZE) 101 | if not data: 102 | break 103 | md5_hash.update(data) 104 | sha256_hash.update(data) 105 | return md5_hash.hexdigest(), sha256_hash.hexdigest() 106 | 107 | 108 | class Output: 109 | """Handles all output for histstat.""" 110 | 111 | def __init__(self, log, json_out, prettify, quiet, hash_mode): 112 | self.log = log 113 | self.json_out = json_out 114 | self.prettify = prettify 115 | self.quiet = quiet 116 | self.hash_mode = hash_mode 117 | 118 | if self.quiet and not self.log: 119 | print('Error: Quiet mode must be used with log mode.') 120 | sys.exit(1) 121 | 122 | if self.prettify and self.json_out: 123 | print('Error: Prettify and JSON output cannot be used together.') 124 | sys.exit(1) 125 | 126 | if self.log: 127 | self.file_handle = open(self.log, 'a') 128 | if quiet: 129 | print(f'Quiet mode enabled, see log file for results: {self.log}') 130 | 131 | if self.hash_mode: 132 | global FIELDS, P_FIELDS 133 | FIELDS = FIELDS[:-1] + ['md5', 'sha256', 'command'] 134 | P_FIELDS = ' '.join( 135 | P_FIELDS.split()[:-1] + ['{:<32}', '{:<64}', '{}'] 136 | ) 137 | 138 | def preflight(self): 139 | root_check = False 140 | 141 | if PLATFORM == 'nix': 142 | euid = geteuid() 143 | if euid == 0: 144 | root_check = True 145 | elif sys.platform == 'darwin': 146 | print('Error: histstat must be run as root on macOS.') 147 | sys.exit(1) 148 | elif PLATFORM == 'win': 149 | if windll.shell32.IsUserAnAdmin() == 0: 150 | root_check = True 151 | 152 | # display netstat-esque privilege level header warning 153 | if not root_check: 154 | print('(Not all process information could be determined, run' \ 155 | ' at a higher privilege level to see everything.)\n') 156 | 157 | # display column names 158 | if not self.json_out: 159 | self.process(FIELDS) 160 | 161 | def process(self, cfields): 162 | if self.hash_mode: 163 | path = cfields[-1].split()[0] 164 | if os.path.isfile(path): 165 | md5_hash, sha256_hash = hash_file(path) 166 | else: 167 | md5_hash, sha256_hash = str(), str() 168 | cfields = cfields[:-1] + [md5_hash, sha256_hash, cfields[-1]] 169 | 170 | if self.prettify: 171 | line = P_FIELDS.format(*cfields) 172 | elif self.json_out: 173 | line = dict(zip(FIELDS, cfields)) 174 | else: 175 | line = '\t'.join(map(str, cfields)) 176 | 177 | # stdout 178 | if not self.quiet: 179 | print(line) 180 | if self.log: 181 | self.file_handle.write(str(line) + '\n') 182 | 183 | 184 | def get_parser(): 185 | parser = argparse.ArgumentParser(description='history for netstat') 186 | parser.add_argument( 187 | '-i', '--interval', 188 | help='specify update interval in seconds', 189 | default=1, type=float 190 | ) 191 | parser.add_argument( 192 | '-j', '--json', 193 | help='json output', 194 | default=False, action='store_true' 195 | ) 196 | parser.add_argument( 197 | '-l', '--log', 198 | help='log output to a file', 199 | default=None, type=str 200 | ) 201 | parser.add_argument( 202 | '-p', '--prettify', 203 | help='prettify output', 204 | default=False, action='store_true' 205 | ) 206 | parser.add_argument( 207 | '-q', '--quiet', 208 | help='quiet mode, do not output to stdout (for use when logging)', 209 | default=False, action='store_true' 210 | ) 211 | parser.add_argument( 212 | '-v', '--version', 213 | help='display the current version', 214 | default=False, action='store_true' 215 | ) 216 | parser.add_argument( 217 | '--hash', 218 | help='takes md5 and sha256 hashes of process files (warning: slow!)', 219 | default=False, action='store_true' 220 | ) 221 | return parser 222 | 223 | 224 | def main(): 225 | parser = get_parser() 226 | args = vars(parser.parse_args()) 227 | 228 | if args['version']: 229 | print(__version__) 230 | return 0 231 | 232 | interval = args['interval'] 233 | 234 | global output 235 | output = Output( 236 | log=args['log'], 237 | json_out=args['json'], 238 | prettify=args['prettify'], 239 | quiet=args['quiet'], 240 | hash_mode=args['hash'] 241 | ) 242 | 243 | try: 244 | histmain(interval) 245 | except KeyboardInterrupt: 246 | pass 247 | 248 | # gracefully stop histstat 249 | if output.log: 250 | output.file_handle.close() 251 | return 0 252 | 253 | 254 | if __name__ == '__main__': 255 | sys.exit(main()) 256 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psutil -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from setuptools import setup 5 | 6 | directory = os.path.abspath(os.path.dirname(__file__)) 7 | with open(os.path.join(directory, 'README.md'), encoding='utf-8') as f: 8 | long_description = f.read() 9 | 10 | setup( 11 | name='histstat', 12 | packages=['histstat'], 13 | version='1.2.0', 14 | description='History for netstat.', 15 | long_description=long_description, 16 | long_description_content_type='text/markdown', 17 | license='MIT', 18 | url='https://github.com/vesche/histstat', 19 | author='Austin Jackson', 20 | author_email='vesche@protonmail.com', 21 | entry_points={ 22 | 'console_scripts': [ 23 | 'histstat = histstat.histstat:main', 24 | ] 25 | }, 26 | install_requires=['psutil'], 27 | classifiers=[ 28 | 'Development Status :: 5 - Production/Stable', 29 | 'Environment :: Console', 30 | 'Intended Audience :: Information Technology', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Programming Language :: Python :: 2', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3.9', 36 | 'Topic :: Security' 37 | ] 38 | ) --------------------------------------------------------------------------------