├── MANIFEST.in ├── jfscan ├── __init__.py ├── core │ ├── __init__.py │ ├── logging_formatter.py │ ├── validator.py │ ├── arg_handler.py │ ├── resources.py │ └── utils.py ├── modules │ ├── __init__.py │ ├── Masscan.py │ └── Nmap.py ├── __version__.py ├── LICENSE └── __main__.py ├── screenshots ├── help.png ├── logo.png ├── usage1.png ├── usage2.png ├── nmap_scan.png ├── for_dummies.png ├── masscan_nmap.png ├── sample_scan.png ├── usage_example.png ├── advanced_usage.png └── usage_example2.png ├── LICENSE ├── setup.py ├── .gitignore └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE -------------------------------------------------------------------------------- /jfscan/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | pass 5 | -------------------------------------------------------------------------------- /jfscan/core/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | pass 4 | -------------------------------------------------------------------------------- /jfscan/modules/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | pass 4 | -------------------------------------------------------------------------------- /jfscan/__version__.py: -------------------------------------------------------------------------------- 1 | VERSION = (1, 6, 2) 2 | 3 | __version__ = ".".join(map(str, VERSION)) -------------------------------------------------------------------------------- /screenshots/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/help.png -------------------------------------------------------------------------------- /screenshots/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/logo.png -------------------------------------------------------------------------------- /screenshots/usage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/usage1.png -------------------------------------------------------------------------------- /screenshots/usage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/usage2.png -------------------------------------------------------------------------------- /screenshots/nmap_scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/nmap_scan.png -------------------------------------------------------------------------------- /screenshots/for_dummies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/for_dummies.png -------------------------------------------------------------------------------- /screenshots/masscan_nmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/masscan_nmap.png -------------------------------------------------------------------------------- /screenshots/sample_scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/sample_scan.png -------------------------------------------------------------------------------- /screenshots/usage_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/usage_example.png -------------------------------------------------------------------------------- /screenshots/advanced_usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/advanced_usage.png -------------------------------------------------------------------------------- /screenshots/usage_example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullt3r/jfscan/HEAD/screenshots/usage_example2.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 nullter@bugdelivery.com 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. -------------------------------------------------------------------------------- /jfscan/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 nullter@bugdelivery.com 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. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup, find_packages 3 | 4 | from jfscan import __version__ 5 | 6 | # The directory containing this file 7 | HERE = pathlib.Path(__file__).parent 8 | 9 | # The text of the README file 10 | README = (HERE / "README.md").read_text() 11 | 12 | # This call to setup() does all the work 13 | setup( 14 | name="jfscan", 15 | version=__version__.__version__, 16 | description="A Masscan wrapper with some useful modules. I am not responsible for any damages. You are responsible for your own actions. Attacking targets without prior mutual consent is illegal.", 17 | long_description=README, 18 | long_description_content_type="text/markdown", 19 | url="https://github.com/nullt3r/jfscan", 20 | author="nullt3r", 21 | author_email="nullt3r@bugdelivery.com", 22 | license="MIT", 23 | python_requires=">=3.6, <4", 24 | classifiers=[ 25 | "License :: OSI Approved :: MIT License", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.7", 28 | "Programming Language :: Python :: 3.9", 29 | ], 30 | packages=find_packages(), 31 | install_requires=["validators", "requests", "tldextract", "dnspython"], 32 | entry_points={ 33 | "console_scripts": [ 34 | "jfscan=jfscan.__main__:main", 35 | ] 36 | }, 37 | ) 38 | -------------------------------------------------------------------------------- /jfscan/core/logging_formatter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class CustomFormatter(logging.Formatter): 5 | 6 | grey = "\x1b[38;20m" 7 | bold_cyan = "\x1b[1;36m" 8 | cyan = "\x1b[0;36m" 9 | bold_white = "\x1b[1;37m" 10 | bold_white = "\x1b[1;37m" 11 | white = "\x1b[0;37m" 12 | yellow = "\x1b[33;20m" 13 | yellow = "\x1b[33;20m" 14 | red = "\x1b[31;20m" 15 | bold_red = "\x1b[31;1m" 16 | reset = "\x1b[0m" 17 | # format = "[%(asctime)s] [%(levelname)s] [%(module)s.%(funcName)s] - %(message)s" 18 | message = "[%(asctime)s] [%(levelname)s] - %(message)s" 19 | 20 | """ 21 | FORMATS = { 22 | logging.DEBUG: cyan + message + reset, 23 | logging.INFO: f"[%(asctime)s] {green}[%(levelname)s]{reset} - %(message)s", 24 | logging.WARNING: yellow + message + reset, 25 | logging.ERROR: red + message + reset, 26 | logging.CRITICAL: bold_red + message + reset, 27 | } 28 | """ 29 | 30 | FORMATS = { 31 | logging.DEBUG: f"{cyan}[%(asctime)s]{reset} {bold_cyan}[%(levelname)s]{reset} - {cyan}%(message)s{reset}", 32 | logging.INFO: f"[%(asctime)s] {white}[%(levelname)s]{reset} - %(message)s", 33 | logging.WARNING: f"[%(asctime)s] {yellow}[%(levelname)s]{reset} - %(message)s", 34 | logging.ERROR: f"[%(asctime)s] {red}[%(levelname)s]{reset} - %(message)s", 35 | logging.CRITICAL: f"[%(asctime)s] {bold_red}[%(levelname)s]{reset} - %(message)s", 36 | } 37 | 38 | def format(self, record): 39 | log_fmt = self.FORMATS.get(record.levelno) 40 | formatter = logging.Formatter(log_fmt, "%Y-%m-%d %H:%M:%S") 41 | return formatter.format(record) 42 | -------------------------------------------------------------------------------- /jfscan/core/validator.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | import re 3 | 4 | class Validator: 5 | @staticmethod 6 | def is_ipv6(addr): 7 | try: 8 | return type(ipaddress.ip_address(addr)) is ipaddress.IPv6Address 9 | except: 10 | return False 11 | 12 | @staticmethod 13 | def is_ipv4(addr): 14 | try: 15 | return type(ipaddress.ip_address(addr)) is ipaddress.IPv4Address 16 | except: 17 | return False 18 | 19 | @staticmethod 20 | def is_ipv6_cidr(addr): 21 | try: 22 | return type(ipaddress.ip_network(addr, False)) is ipaddress.IPv6Network 23 | except: 24 | return False 25 | 26 | @staticmethod 27 | def is_ipv4_cidr(addr): 28 | try: 29 | return type(ipaddress.ip_network(addr, False)) is ipaddress.IPv4Network 30 | except: 31 | return False 32 | 33 | @staticmethod 34 | def is_ipv4_range(ip_range): 35 | ip_range = ip_range.split("-") 36 | try: 37 | if (type(ipaddress.ip_address(ip_range[0])) is ipaddress.IPv4Address 38 | and type(ipaddress.ip_address(ip_range[1])) is ipaddress.IPv4Address): 39 | return True 40 | except: 41 | return False 42 | 43 | @staticmethod 44 | def is_ipv6_range(ip_range): 45 | ip_range = ip_range.split("-") 46 | try: 47 | if (type(ipaddress.ip_address(ip_range[0])) is ipaddress.IPv6Address 48 | and type(ipaddress.ip_address(ip_range[1])) is ipaddress.IPv6Address): 49 | return True 50 | except: 51 | return False 52 | 53 | @staticmethod 54 | def is_mac(mac) -> bool: 55 | is_valid_mac = re.match(r'([0-9A-F]{2}[:]){5}[0-9A-F]{2}|' 56 | r'([0-9A-F]{2}[-]){5}[0-9A-F]{2}', 57 | string=mac, 58 | flags=re.IGNORECASE) 59 | try: 60 | return bool(is_valid_mac.group()) 61 | except AttributeError: 62 | return False 63 | 64 | @staticmethod 65 | def is_url(url): 66 | if url.startswith("http://") or url.startswith("https://") is True: 67 | return True 68 | return False 69 | 70 | @staticmethod 71 | def is_domain(host): 72 | from validators import domain 73 | return domain(host) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ffs 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .venv 120 | env/ 121 | venv/ 122 | ENV/ 123 | env.bak/ 124 | venv.bak/ 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ 143 | 144 | # pytype static type analyzer 145 | .pytype/ 146 | 147 | # Cython debug symbols 148 | cython_debug/ 149 | 150 | # PyCharm 151 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 152 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 153 | # and can be added to the global gitignore or merged into this file. For a more nuclear 154 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 155 | #.idea/ 156 | -------------------------------------------------------------------------------- /jfscan/modules/Masscan.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | class Masscan: 5 | def __init__(self, utils): 6 | self.logger = logging.getLogger(__name__) 7 | self.utils = utils 8 | 9 | self.rate = None 10 | self.wait = None 11 | self.ports = None 12 | self.top_ports = None 13 | self.interface = None 14 | self.router_ip = None 15 | self.source_ip = None 16 | self.router_mac = None 17 | self.router_mac_ipv6 = None 18 | 19 | def run(self, resources): 20 | """ 21 | Description: Native module for identification of open ports, uses Masscan 22 | Author: nullt3r 23 | 24 | """ 25 | logger = self.logger 26 | utils = self.utils 27 | 28 | interface = self.interface 29 | router_ip = self.router_ip 30 | source_ip = self.source_ip 31 | router_mac = self.router_mac 32 | router_mac_ipv6 = self.router_mac_ipv6 33 | top_ports = self.top_ports 34 | ports = self.ports 35 | rate = self.rate 36 | wait = self.wait 37 | 38 | logger.info("port scanning using masscan started") 39 | 40 | stream_output = bool(logging.INFO >= logging.root.level) 41 | 42 | ips = resources.get_ips() 43 | cidrs = resources.get_cidrs() 44 | 45 | if len(ips) == 0 and len(cidrs) == 0: 46 | logger.error("no resources were given, nothing to scan") 47 | raise SystemExit(1) 48 | 49 | masscan_input = f"/tmp/_jfscan_{utils.random_string()}" 50 | 51 | with open(masscan_input, "a") as f: 52 | if len(ips) != 0: 53 | for (ip,) in ips: 54 | f.write(f"{ip}\n") 55 | 56 | if len(cidrs) != 0: 57 | for (cidr,) in cidrs: 58 | f.write(f"{cidr}\n") 59 | 60 | result = utils.handle_command( 61 | f"masscan{' --wait ' + str(wait) if wait is not None else ''}{' --interface ' + interface if interface is not None else ''}{' --source-ip ' + source_ip if source_ip is not None else ''}{' --router-mac ' + router_mac if router_mac is not None else ''}{' --router-mac-ipv6 ' + router_mac_ipv6 if router_mac_ipv6 is not None else ''}{' --router-ip ' + router_ip if router_ip is not None else ''}{' --ports ' + ports if top_ports is None else ' --top-ports ' + str(top_ports)} --open --max-rate {rate} -iL {masscan_input}", 62 | stream_output, 63 | ) 64 | 65 | result_stderr = result.stderr.decode("utf-8") 66 | 67 | if "FAIL: could not determine default interface" in result_stderr: 68 | logger.fatal( 69 | "could not determine default interface, specify it using --interface " 70 | ) 71 | raise SystemExit(1) 72 | 73 | if "FAIL: scan range too large, max is" in result_stderr: 74 | logger.fatal( 75 | "scan range too large, are you trying to scan large IPv6 network?" 76 | ) 77 | raise SystemExit(1) 78 | 79 | if "FAIL: failed to detect IPv6 address of interface" in result_stderr: 80 | logger.fatal( 81 | "are you sure you have IPv6? Try to specify --router-mac-ipv6 ($ ip neigh) or --source-ip " 82 | ) 83 | raise SystemExit(1) 84 | 85 | if "BIOCSETIF failed: Device not configured" in result_stderr: 86 | logger.fatal( 87 | "interface %s does not exists or can't be used for scanning", interface 88 | ) 89 | raise SystemExit(1) 90 | 91 | if "FAIL: failed to detect IP of interface" in result_stderr: 92 | logger.fatal("interface %s has no IP address set", interface) 93 | raise SystemExit(1) 94 | 95 | if ( 96 | "FAIL: ARP timed-out resolving MAC address for router" 97 | in result_stderr 98 | ): 99 | logger.fatal( 100 | "can't resolve MAC address for router, please specify --router-ip " 101 | ) 102 | raise SystemExit(1) 103 | 104 | result_stdout = result.stdout.decode("utf-8") 105 | 106 | if "Discovered open port " not in result_stdout: 107 | logger.info( 108 | "no open ports were discovered (maybe something went wrong with your connection?)" 109 | ) 110 | raise SystemExit(1) 111 | 112 | for line in result_stdout.splitlines(): 113 | if line.startswith("Discovered open port "): 114 | items = line.split(" ") 115 | 116 | protocol = items[3].split("/")[1] 117 | port = items[3].split("/")[0] 118 | ip = items[5] 119 | 120 | resources.report_port(ip, port, protocol) 121 | -------------------------------------------------------------------------------- /jfscan/modules/Nmap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import os 4 | import multiprocessing 5 | 6 | 7 | class Nmap: 8 | def __init__(self, utils): 9 | self.logger = logging.getLogger(__name__) 10 | self.utils = utils 11 | 12 | self.interface = None 13 | self.options = None 14 | self.output = None 15 | self.threads = 8 16 | 17 | def _run_single_nmap(self, _args): 18 | utils = self.utils 19 | 20 | from jfscan.core.validator import Validator 21 | from jfscan.core.logging_formatter import CustomFormatter 22 | 23 | logger = logging.getLogger() 24 | stream_handler = logging.StreamHandler() 25 | stream_handler.setFormatter(CustomFormatter()) 26 | logger.addHandler(stream_handler) 27 | 28 | domains, host, ports, options, interface, output = _args 29 | 30 | if len(ports) == 0: 31 | return 32 | 33 | if len(ports) > 350: 34 | logger.warning( 35 | "host %s has %s of open ports, probably firewall messing with us - not scanning", 36 | host, 37 | len(ports), 38 | ) 39 | return 40 | 41 | ports = ",".join(map(str, ports)) 42 | stdout_buffer = "" 43 | 44 | if output is not None: 45 | nmap_output = '/tmp/_jfscan_' + utils.random_string() + '.xml' 46 | 47 | result = utils.handle_command( 48 | f"nmap{' -e ' + interface if interface is not None else ''}{' -6 ' if Validator.is_ipv6(host) is True else ''} --noninteractive -Pn {host} -p {ports}{' '+options if options is not None else ''}{' -oX ' + nmap_output if output is not None else ''}" 49 | ) 50 | 51 | result_stdout = result.stdout.decode("utf-8") 52 | result_stderr = result.stderr.decode("utf-8") 53 | 54 | if ( 55 | "I cannot figure out what source address to use for device" 56 | in result_stderr 57 | ): 58 | logger.fatal("interface does not exists or can't be used for scanning") 59 | raise SystemExit(1) 60 | 61 | if "Could not find interface" in result_stderr: 62 | logger.fatal("interface does not exists or can't be used for scanning") 63 | raise SystemExit(1) 64 | 65 | if len(domains) == 0: 66 | f_host_domain = f" {host} " 67 | else: 68 | f_host_domain = f" {host} ({', '.join([domain for domain in domains])}) " 69 | 70 | stdout_buffer += "┌──────\033[1m" + f_host_domain + "\033[0m\n" 71 | stdout_buffer += "│" 72 | 73 | if ( 74 | "Nmap done: 1 IP address (0 hosts up)" in result_stdout 75 | or result.returncode != 0 76 | ): 77 | stdout_buffer += "Host is down.\n\n" 78 | 79 | logger.error("Host %s seems down, but was up while scanning with the masscan, maybe your network connection is not able to handle the scanning, \nare you scanning over a wifi? Try VPS or ethernet instead.\n\n", host) 80 | else: 81 | nmap_stdout = "\n│ ".join(result_stdout.splitlines()[3:][:-2]) + "\n" 82 | 83 | output_in_colors = nmap_stdout.replace( 84 | " open ", "\033[1m\033[92m open \033[0m" 85 | ) 86 | output_in_colors = output_in_colors.replace( 87 | " filtered ", "\033[1m\033[93m filtered \033[0m" 88 | ) 89 | output_in_colors = output_in_colors.replace( 90 | " closed ", "\033[1m\033[91m closed \033[0m" 91 | ) 92 | 93 | stdout_buffer += output_in_colors 94 | 95 | print(stdout_buffer) 96 | 97 | if output is not None: 98 | if utils.file_is_empty(nmap_output): 99 | return None 100 | 101 | return nmap_output 102 | 103 | def run(self, resources): 104 | logger = self.logger 105 | 106 | threads = self.threads 107 | options = self.options 108 | interface = self.interface 109 | output = self.output 110 | 111 | logger.info("service discovery using nmap started\n") 112 | 113 | nmap_input = resources.get_results_complex() 114 | 115 | if len(nmap_input) == 0: 116 | logger.error("no resources were given, nothing to scan") 117 | return 118 | 119 | process_pool = multiprocessing.Pool(processes=threads) 120 | run = process_pool.map( 121 | self._run_single_nmap, 122 | [target + (options, interface, output) for target in nmap_input], 123 | ) 124 | process_pool.close() 125 | 126 | if output is not None: 127 | logger.info("generating report %s", output) 128 | 129 | host_report = [] 130 | on_first_run = 0 131 | 132 | for xml_report in run: 133 | if xml_report is None: 134 | continue 135 | with open(xml_report, "r") as thread_output: 136 | _reader = thread_output.readlines() 137 | 138 | if on_first_run == 0: 139 | extract_stylesheet = _reader[2].split('"')[1] 140 | on_first_run = 1 141 | 142 | host_report.append("".join(_reader[8:][:-3])) 143 | 144 | try: 145 | os.remove(xml_report) 146 | except: 147 | pass 148 | 149 | report_header = f""" 150 | 151 | 152 | 153 | 154 | 155 | """ 156 | 157 | report_end = """ 158 | 159 | """ 160 | 161 | with open(output, "w") as output: 162 | output.write(report_header + "\n".join(host_report) + report_end) 163 | -------------------------------------------------------------------------------- /jfscan/__main__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=import-error 2 | #!/usr/bin/env python3 3 | import logging 4 | import time 5 | 6 | from jfscan.core.resources import Resources 7 | from jfscan.core.utils import Utils 8 | from jfscan.core.arg_handler import ArgumentHandler 9 | from jfscan.core.logging_formatter import CustomFormatter 10 | 11 | from jfscan.modules.Masscan import Masscan 12 | from jfscan.modules.Nmap import Nmap 13 | 14 | from jfscan import __version__ 15 | 16 | CURRENT_VERSION = __version__.__version__ 17 | 18 | 19 | def main(): 20 | try: 21 | # Setup logging 22 | logger = logging.getLogger() 23 | stream_handler = logging.StreamHandler() 24 | stream_handler.setFormatter(CustomFormatter()) 25 | logger.addHandler(stream_handler) 26 | 27 | # Handle arguments 28 | arguments = ArgumentHandler() 29 | 30 | ports_count = 0 31 | 32 | # Set the debug level 33 | if arguments.quite is True: 34 | logger.level = logging.ERROR 35 | else: 36 | if arguments.verbose is True: 37 | logger.level = logging.DEBUG 38 | else: 39 | logger.level = logging.INFO 40 | 41 | print( 42 | f"""\033[38;5;63m 43 | ___,__, _, _,_ , , 44 | ',| '|_,(_, / '|\ |\ | 45 | (_| | _)'\_ |-\ |'\| 46 | ' ' `' `' ` \033[0m 47 | \033[97mversion: {CURRENT_VERSION} / author: @nullt3r\033[0m 48 | """ 49 | ) 50 | 51 | # Set arguments for Utils first 52 | utils = Utils() 53 | 54 | if arguments.resolvers is not None: 55 | user_resolvers = arguments.resolvers.split(",") 56 | logger.info("using custom resolvers: %s", ", ".join(user_resolvers)) 57 | utils.resolvers = user_resolvers 58 | 59 | if arguments.enable_ipv6 is True: 60 | logger.info("enabling IPv6 support") 61 | utils.enable_ipv6 = arguments.enable_ipv6 62 | 63 | # Create new instance of the modules with a prepared Utils class. 64 | # Is there a better way? 65 | res = Resources(utils) 66 | masscan = Masscan(utils) 67 | nmap = Nmap(utils) 68 | 69 | # Set additional parameters for Resources 70 | if arguments.scope is not None: 71 | logger.info("targets will be validated against scope defined in file %s", arguments.scope) 72 | res.scope_file = arguments.scope 73 | 74 | # Set additional parameters for Masscan 75 | if arguments.interface is not None: 76 | masscan.interface = arguments.interface 77 | 78 | if arguments.wait is not None: 79 | masscan.wait = arguments.wait 80 | 81 | if arguments.router_ip is not None: 82 | masscan.router_ip = arguments.router_ip 83 | 84 | if arguments.router_mac is not None: 85 | masscan.router_mac = arguments.router_mac 86 | 87 | if arguments.router_mac_ipv6 is not None: 88 | masscan.router_mac_ipv6 = arguments.router_mac_ipv6 89 | 90 | if arguments.source_ip is not None: 91 | masscan.source_ip = arguments.source_ip 92 | 93 | if arguments.top_ports is not None: 94 | ports_count += arguments.top_ports 95 | masscan.top_ports = arguments.top_ports 96 | 97 | if arguments.ports is not None: 98 | masscan.ports = arguments.ports 99 | for _port in arguments.ports.split(","): 100 | if "-" in _port: 101 | high_port = 65535 if _port.split("-")[1].strip() == "" else int(_port.split("-")[1]) 102 | low_port = 0 if _port.split("-")[0].strip() == "" else int(_port.split("-")[0]) 103 | ports_count += high_port - low_port 104 | else: 105 | ports_count += 1 106 | 107 | if arguments.yummy_ports is True: 108 | yummy_ports = utils.yummy_ports() 109 | ports_count += len(yummy_ports) 110 | masscan.ports = ",".join(map(str, yummy_ports)) 111 | 112 | # Check dependencies 113 | utils.check_dependency("nmap", "--version", "Nmap version 7.") 114 | utils.check_dependency("masscan", "--version", "1.3.2") 115 | 116 | # Load targets specified by user 117 | utils.load_targets( 118 | res, 119 | targets_file=arguments.targets, 120 | target=arguments.target.split(",") 121 | if arguments.target is not None 122 | else None, 123 | ) 124 | 125 | # Count all the possible IPs to be scanned for the auto-rate feature 126 | ip_count = res.count_ips() 127 | 128 | if ip_count == 0: 129 | logger.error("nothing to scan, no domains were resolved") 130 | raise SystemExit(1) 131 | elif ip_count > 2**32: 132 | logger.fatal("number of IPs to be scanned is very large (%s to be exact), you probably specified wrong IPv6 network range...", ip_count) 133 | raise SystemExit(1) 134 | 135 | # Lets continue if number of IPs to be scanned is acceptable 136 | logger.info("%s unique IP addresses will be scanned", ip_count) 137 | 138 | # Set another parameters to masscan: adjust masscan's rate 139 | if arguments.disable_auto_rate is False: 140 | computed_rate = utils.compute_rate( 141 | ip_count, ports_count, arguments.max_rate 142 | ) 143 | logger.info( 144 | "adjusting packet rate to %s kpps (you can disable this by --disable-auto-rate)", 145 | computed_rate, 146 | ) 147 | masscan.rate = computed_rate 148 | else: 149 | logger.info("rate adjustment disabled, some open ports might not be discovered") 150 | masscan.rate = arguments.max_rate 151 | 152 | scanning_start = time.perf_counter() 153 | 154 | masscan.run(res) 155 | 156 | # Report results from masscan 157 | logger.info("showing results") 158 | 159 | results = [] 160 | result_ips, result_domains = res.get_scan_results() 161 | 162 | if arguments.only_domains is True: 163 | results = result_domains 164 | elif arguments.only_ips is True: 165 | results = result_ips 166 | else: 167 | results = result_ips + result_domains 168 | 169 | for line in results: 170 | print(line) 171 | 172 | # Save results to file 173 | if arguments.output is not None: 174 | logger.info("saving results to %s", arguments.output) 175 | utils.save_results(results, arguments.output) 176 | 177 | # Are we going to run nmap also? Set arguments for nmap 178 | if arguments.nmap is True: 179 | if arguments.interface is not None: 180 | nmap.interface = arguments.interface 181 | 182 | if arguments.nmap_output is not None: 183 | nmap.output = arguments.nmap_output 184 | 185 | if arguments.nmap_threads is not None: 186 | nmap.threads = arguments.nmap_threads 187 | 188 | if arguments.nmap_options is not None: 189 | nmap.options = arguments.nmap_options 190 | 191 | nmap.run(res) 192 | 193 | scanning_stop = time.perf_counter() 194 | 195 | logger.info( 196 | "scan took %0.2f seconds, discovered %s open ports, %s hosts alive out of %s total", 197 | scanning_stop - scanning_start, 198 | res.count_ports(), 199 | res.count_alive_ips(), 200 | ip_count 201 | ) 202 | 203 | except KeyboardInterrupt: 204 | logger.fatal("ctrl+c was pressed, cleaning up & exiting...") 205 | 206 | import os, glob 207 | 208 | for jfscan_file in glob.glob("/tmp/_jfscan_*"): 209 | os.remove(jfscan_file) 210 | 211 | raise SystemExit(1) 212 | 213 | 214 | if __name__ == "__main__": 215 | main() 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](screenshots/logo.png) 2 | ![GitHub](https://img.shields.io/github/license/nullt3r/jfscan) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/nullt3r/jfscan) ![Rating](https://img.shields.io/github/stars/nullt3r/jfscan?style=social) 3 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 4 | 5 | # Description 6 | ## Killing features 7 | * Unleash the power of Nmap with Masscan for large-scale scans 8 | * Scan targets using domain names and other formats 9 | * Output results in a clean domain:port format 10 | * Stream results to and from other tools using stdin/stdout mode 11 | * Enjoy hassle-free scanning with automatic packet rate adjustment for Masscan 12 | * Generate Nmap XML reports 13 | * Stay focused and on target with scope control 14 | 15 | JFScan is a wrapper that leverages the speed of Masscan and Nmap's fingerprinting capabilities. JFScan accepts targets in the form of URLs, domains, or IPs (including CIDR). You can specify a file with targets using an argument, or use stdin. 16 | 17 | JFScan also allows you to output only the results and chain them with other tools like Nuclei. The domain:port output of JFScan is crucial for identifying vulnerabilities in web applications, as the virtual host determines which content will be served. 18 | 19 | In addition, JFScan can scan discovered ports with Nmap, and enables you to define custom options and leverage Nmap's advanced scripting capabilities. 20 | 21 | 22 | ![nmap](screenshots/usage1.png) 23 | ![usage2](screenshots/usage2.png) 24 | 25 | JFScans logic of input & output processing: 26 | 27 | ![diagram](screenshots/for_dummies.png) 28 | 29 | # Usage 30 | ``` 31 | usage: jfscan [-h] [--targets TARGETS] (-p PORTS | --top-ports TOP_PORTS | --yummy-ports) [--resolvers RESOLVERS] [--enable-ipv6] [--scope SCOPE] [-r MAX_RATE] [--wait WAIT] [--disable-auto-rate] [-i INTERFACE] [--source-ip SOURCE_IP] 32 | [--router-ip ROUTER_IP] [--router-mac ROUTER_MAC] [--router-mac-ipv6 ROUTER_MAC_IPV6] [-oi] [-od] [-o OUTPUT] [-q | -v] [--nmap] [--nmap-options NMAP_OPTIONS] [--nmap-threads NMAP_THREADS] [--nmap-output NMAP_OUTPUT] [--version] 33 | [target] 34 | 35 | JFScan - Just Fu*king Scan 36 | 37 | optional arguments: 38 | -h, --help show this help message and exit 39 | -p PORTS, --ports PORTS 40 | ports, can be a range or port list: 0-65535 or 22,80,100-500,... 41 | --top-ports TOP_PORTS 42 | scan only N of the top ports, e. g., --top-ports 1000 43 | --yummy-ports scan only for the most yummy ports 44 | -q, --quite output only results 45 | -v, --verbose verbose output 46 | 47 | --nmap run nmap on discovered ports 48 | --nmap-options NMAP_OPTIONS 49 | nmap arguments, e. g., --nmap-options='-sV' or --nmap-options='-sV --script ssh-auth-methods' 50 | --nmap-threads NMAP_THREADS 51 | number of nmaps to run concurrently, default 8 52 | --nmap-output NMAP_OUTPUT 53 | output results from nmap to specified file in standard XML format (same as nmap option -oX) 54 | 55 | target a target or targets separated by a comma, accepted form is: domain name, IPv4, IPv6, URL 56 | --targets TARGETS file with targets, accepted form is: domain name, IPv4, IPv6, URL 57 | 58 | -oi, --only-ips output only IP adresses, default: all resources 59 | -od, --only-domains output only domains, default: all resources 60 | -o OUTPUT, --output OUTPUT 61 | output masscan's results to specified file 62 | 63 | --resolvers RESOLVERS 64 | custom resolvers separated by a comma, e. g., 8.8.8.8,1.1.1.1 65 | --enable-ipv6 enable IPv6 support, otherwise all IPv6 addresses will be ignored in the scanning process 66 | --scope SCOPE file path with IP adresses and CIDRs to control scope, expected format: IPv6, IPv4, IPv6 CIDR, IPv4 CIDR 67 | -r MAX_RATE, --max-rate MAX_RATE 68 | max kpps rate for the masscan 69 | --wait WAIT a number of seconds to wait for packets to arrive (when scanning large networks), option for the masscan 70 | --disable-auto-rate disable rate adjustment mechanism for masscan (more false positives/negatives) 71 | -i INTERFACE, --interface INTERFACE 72 | interface for masscan and nmap to use 73 | --source-ip SOURCE_IP 74 | IP address of your interface for the masscan 75 | --router-ip ROUTER_IP 76 | IP address of your router for the masscan 77 | --router-mac ROUTER_MAC 78 | MAC address of your router for the masscan 79 | --router-mac-ipv6 ROUTER_MAC_IPV6 80 | MAC address of your IPv6 router for the masscan 81 | 82 | --version show program's version number and exit 83 | ``` 84 | 85 | Please follow installation instructions before running. Do not run the JFScan under a root, it's not needed since we set a special permissions on the masscan binary. 86 | 87 | ## Example 88 | Scan targets for only for ports 80 and 443 with rate of 10 kpps: 89 | 90 | `$ jfscan -p 80,443 --targets targets.txt -r 10000` 91 | 92 | Scan targets for top 1000 ports : 93 | 94 | `$ jfscan --top-ports 1000 1.1.1.1/24` 95 | 96 | You can also specify targets on stdin and pipe it to nuclei: 97 | 98 | `$ cat targets.txt | jfscan --top-ports 1000 -q | httpx -silent | nuclei` 99 | 100 | Or as positional parameter: 101 | 102 | `$ jfscan --top-ports 1000 1.1.1.1/24 -q | httpx -silent | nuclei` 103 | 104 | Or everything at once, the JFScan just does not care and scans all the targets specified: 105 | 106 | `$ echo target1 | jfscan --top-ports 1000 target2 --targets targets.txt -q | httpx -silent | nuclei` 107 | 108 | Utilize nmap to gather more info about discovered services: 109 | 110 | `$ cat targets.txt | jfscan -p 0-65535 --nmap --nmap-options="-sV --scripts ssh-auth-methods"` 111 | 112 | The targets.txt can contain targets in the following forms (IPv6 similarly): 113 | ``` 114 | http://domain.com/ 115 | domain.com 116 | 1.2.3.4 117 | 1.2.3.0/24 118 | 1.1.1.1-1.1.1.30 119 | ``` 120 | 121 | # Installation 122 | 1. Before installation, make sure you have the latest version of Masscan installed (tested version is 1.3.2). 123 | 124 | First, install a libpcap-dev (Debian based distro) or libcap-devel (Centos based distro): 125 | 126 | ``` 127 | sudo apt install libpcap-dev 128 | ``` 129 | 130 | Next, clone the official repository and install: 131 | ``` 132 | sudo apt-get --assume-yes install git make gcc 133 | git clone https://github.com/robertdavidgraham/masscan 134 | cd masscan 135 | make 136 | sudo make install 137 | ``` 138 | 139 | 140 | 1. The Masscan requires root permissions to run. Since running binaries under root is not good idea, we will set a CAP_NET_RAW capability to the binary: 141 | 142 | ``` 143 | sudo setcap CAP_NET_RAW+ep /usr/bin/masscan 144 | ``` 145 | 146 | 3. For installation of JFscan a python3 and pip3 is required. 147 | 148 | ``` 149 | sudo apt install python3 python3-pip 150 | ``` 151 | 152 | 4. Install JFScan: 153 | ``` 154 | $ git clone https://github.com/nullt3r/jfscan.git 155 | $ cd jfscan 156 | $ pip3 install . 157 | ``` 158 | If you can't run the jfscan directly from command line you should check if $HOME/.local/bin is in your path. 159 | 160 | Add the following line to your `~/.zshrc` or `~/.bashrc`: 161 | 162 | ``` 163 | export PATH="$HOME/.local/bin:$PATH" 164 | ``` 165 | 166 | # License 167 | Read file LICENSE. 168 | 169 | # Disclaimer 170 | I am not responsible for any damages. You are responsible for your own 171 | actions. Attacking targets without prior mutual consent is illegal. 172 | ___ 173 | 174 | \* *When scanning smaller network ranges, you can just use nmap directly, there is no need to use JFScan. You can reach up to 70% of the speed of JFScan using the following options:* 175 | ``` 176 | nmap -Pn -n -v yourTargetNetwork/26 -p- --min-parallelism 64 --min-rate 20000 --min-hostgroup 64 --randomize-hosts -sS -sV 177 | ``` 178 | *As always, expect some false positivies/negatives.* 179 | -------------------------------------------------------------------------------- /jfscan/core/arg_handler.py: -------------------------------------------------------------------------------- 1 | import validators 2 | import argparse 3 | import subprocess 4 | import sys 5 | import re 6 | 7 | from jfscan import __version__ 8 | from jfscan.core.validator import Validator 9 | 10 | CURRENT_VERSION = __version__.__version__ 11 | 12 | class ArgumentHandler: 13 | def __init__(self): 14 | is_tty = bool(sys.stdin.isatty()) 15 | 16 | parser = argparse.ArgumentParser(description="JFScan - Just Fu*king Scan") 17 | 18 | group_ports = parser.add_mutually_exclusive_group(required=True) 19 | group_logging = parser.add_mutually_exclusive_group(required=False) 20 | group_nmap = parser.add_argument_group() 21 | group_targets = parser.add_argument_group() 22 | group_output = parser.add_argument_group() 23 | group_scan_settings = parser.add_argument_group() 24 | group_version = parser.add_argument_group() 25 | 26 | group_targets.add_argument( 27 | "target", 28 | action="store", 29 | help="a target or targets separated by a comma, accepted form is: domain name, IPv4, IPv6, URL", 30 | nargs="?", 31 | ) 32 | group_targets.add_argument( 33 | "--targets", 34 | action="store", 35 | help="file with targets, accepted form is: domain name, IPv4, IPv6, URL", 36 | required=False, 37 | ) 38 | group_ports.add_argument( 39 | "-p", 40 | "--ports", 41 | action="store", 42 | help="ports, can be a range or port list: 0-65535 or 22,80,100-500,...", 43 | required=False, 44 | ) 45 | group_ports.add_argument( 46 | "--top-ports", 47 | action="store", 48 | type=int, 49 | help="scan only N of the top ports, e. g., --top-ports 1000", 50 | required=False, 51 | ) 52 | group_ports.add_argument( 53 | "--yummy-ports", 54 | action="store_true", 55 | help="scan only for the most yummy ports", 56 | required=False, 57 | ) 58 | group_scan_settings.add_argument( 59 | "--resolvers", 60 | action="store", 61 | help="custom resolvers separated by a comma, e. g., 8.8.8.8,1.1.1.1", 62 | required=False, 63 | ) 64 | group_scan_settings.add_argument( 65 | "--enable-ipv6", 66 | action="store_true", 67 | help="enable IPv6 support, otherwise all IPv6 addresses will be ignored in the scanning process", 68 | required=False, 69 | ) 70 | group_scan_settings.add_argument( 71 | "--scope", 72 | action="store", 73 | help="file path with IP adresses and CIDRs to control scope, expected format: IPv6, IPv4, IPv6 CIDR, IPv4 CIDR", 74 | required=False, 75 | ) 76 | group_scan_settings.add_argument( 77 | "-r", 78 | "--max-rate", 79 | action="store", 80 | type=int, 81 | default=30000, 82 | help="max kpps rate for the masscan", 83 | required=False, 84 | ) 85 | group_scan_settings.add_argument( 86 | "--wait", 87 | action="store", 88 | type=int, 89 | default=10, 90 | help="a number of seconds to wait for packets to arrive (when scanning large networks), option for the masscan", 91 | required=False, 92 | ) 93 | group_scan_settings.add_argument( 94 | "--disable-auto-rate", 95 | action="store_true", 96 | help="disable rate adjustment mechanism for masscan (more false positives/negatives)", 97 | required=False, 98 | ) 99 | group_scan_settings.add_argument( 100 | "-i", 101 | "--interface", 102 | action="store", 103 | help="interface for masscan and nmap to use", 104 | required=False, 105 | ) 106 | group_scan_settings.add_argument( 107 | "--source-ip", 108 | action="store", 109 | help="IP address of your interface for the masscan", 110 | required=False, 111 | ) 112 | group_scan_settings.add_argument( 113 | "--router-ip", 114 | action="store", 115 | help="IP address of your router for the masscan", 116 | required=False, 117 | ) 118 | group_scan_settings.add_argument( 119 | "--router-mac", 120 | action="store", 121 | help="MAC address of your router for the masscan", 122 | required=False, 123 | ) 124 | group_scan_settings.add_argument( 125 | "--router-mac-ipv6", 126 | action="store", 127 | help="MAC address of your IPv6 router for the masscan", 128 | required=False, 129 | ) 130 | group_output.add_argument( 131 | "-oi", 132 | "--only-ips", 133 | action="store_true", 134 | help="output only IP adresses, default: all resources", 135 | required=False, 136 | ) 137 | group_output.add_argument( 138 | "-od", 139 | "--only-domains", 140 | action="store_true", 141 | help="output only domains, default: all resources", 142 | required=False, 143 | ) 144 | group_output.add_argument( 145 | "-o", 146 | "--output", 147 | action="store", 148 | help="output masscan's results to specified file", 149 | required=False, 150 | ) 151 | group_logging.add_argument( 152 | "-q", 153 | "--quite", 154 | action="store_true", 155 | help="output only results", 156 | required=False, 157 | ) 158 | group_logging.add_argument( 159 | "-v", 160 | "--verbose", 161 | action="store_true", 162 | help="verbose output", 163 | required=False, 164 | ) 165 | group_nmap.add_argument( 166 | "--nmap", 167 | action="store_true", 168 | help="run nmap on discovered ports", 169 | ) 170 | group_nmap.add_argument( 171 | "--nmap-options", 172 | action="store", 173 | help="nmap arguments, e. g., --nmap-options='-sV' or --nmap-options='-sV --script ssh-auth-methods'", 174 | ) 175 | group_nmap.add_argument( 176 | "--nmap-threads", 177 | action="store", 178 | type=int, 179 | help="number of nmaps to run concurrently, default 8", 180 | ) 181 | group_nmap.add_argument( 182 | "--nmap-output", 183 | action="store", 184 | help="output results from nmap to specified file in standard XML format (same as nmap option -oX)", 185 | ) 186 | group_version.add_argument( 187 | "--version", action="version", version=CURRENT_VERSION 188 | ) 189 | 190 | args = parser.parse_args() 191 | 192 | if (args.targets or args.target) is None: 193 | if is_tty is True: 194 | parser.error( 195 | "the following arguments are required: --targets, positional parameter [target] or stdin, you can also combine all options" 196 | ) 197 | 198 | if args.router_ip is not None: 199 | if Validator.is_ipv4(args.router_ip) or Validator.is_ipv6(args.router_ip) is not True: 200 | parser.error("--router-ip has to be an IP addresses") 201 | 202 | if args.source_ip is not None: 203 | if Validator.is_ipv4(args.source_ip) or Validator.is_ipv6(args.source_ip) is not True: 204 | parser.error("--source-ip has to be an IP addresses") 205 | 206 | if args.router_mac is not None: 207 | if Validator.is_mac(args.router_mac) is not True: 208 | parser.error("--router-mac has to be an MAC addresses") 209 | 210 | if args.router_mac_ipv6 is not None: 211 | if args.enable_ipv6 is False: 212 | parser.error("you have to enable ipv6 by --enable-ipv6 before using option --router-mac-ipv6") 213 | if Validator.is_mac(args.router_mac_ipv6) is not True: 214 | parser.error("--router-mac-ipv6 has to be an MAC addresses") 215 | 216 | if args.ports is not None: 217 | port_chars = re.compile(r"^[0-9,\-]+$") 218 | if not re.search(port_chars, args.ports): 219 | parser.error("ports are in a wrong format") 220 | 221 | if args.nmap: 222 | if args.nmap_options is not None: 223 | if any( 224 | _opt in args.nmap_options for _opt in ["-oN", "-oS", "-oX", "-oG"] 225 | ): 226 | parser.error( 227 | "output arguments -oNSXG are not permitted, you can use option --nmap-output to save all results to a single xml file (like -oX)" 228 | ) 229 | 230 | result = subprocess.run( 231 | f"nmap --noninteractive -p 65532 127.0.0.1 {args.nmap_options} {'-e ' + args.interface if args.interface is not None else ''}", 232 | capture_output=True, 233 | shell=True, 234 | check=False, 235 | ) 236 | 237 | if result.returncode != 0: 238 | error = result.stderr.decode() 239 | parser.error(f"incorrect nmap options: \n{error}") 240 | 241 | if args.resolvers is not None: 242 | for resolver in args.resolvers.split(","): 243 | if (validators.ipv4(resolver) or validators.ipv6(resolver)) is not True: 244 | parser.error("resolvers must be specified as IP addresses") 245 | 246 | self.quite = args.quite 247 | self.verbose = args.verbose 248 | self.scope = args.scope 249 | self.enable_ipv6 = args.enable_ipv6 250 | self.ports = args.ports 251 | self.top_ports = args.top_ports 252 | self.yummy_ports = args.yummy_ports 253 | self.resolvers = args.resolvers 254 | self.max_rate = args.max_rate 255 | self.wait = args.wait 256 | self.disable_auto_rate = args.disable_auto_rate 257 | self.interface = args.interface 258 | self.source_ip = args.source_ip 259 | self.router_ip = args.router_ip 260 | self.router_mac = args.router_mac 261 | self.router_mac_ipv6 = args.router_mac_ipv6 262 | self.targets = args.targets 263 | self.target = args.target 264 | self.only_domains = args.only_domains 265 | self.only_ips = args.only_ips 266 | self.output = args.output 267 | self.nmap = args.nmap 268 | self.nmap_options = args.nmap_options 269 | self.nmap_threads = args.nmap_threads 270 | self.nmap_output = args.nmap_output 271 | 272 | 273 | -------------------------------------------------------------------------------- /jfscan/core/resources.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import sqlite3 4 | import ipaddress 5 | 6 | from jfscan.core.validator import Validator 7 | 8 | class Resources(): 9 | def __init__(self, utils): 10 | self.logger = logging.getLogger(__name__) 11 | self.utils = utils 12 | self.scope_file = None 13 | 14 | try: 15 | self.conn = sqlite3.connect(":memory:") 16 | except Exception: 17 | self.logger.fatal("%s could not create database", bin) 18 | raise SystemExit(1) 19 | 20 | cur = self.conn.cursor() 21 | 22 | domains_to_scan = "CREATE TABLE domains_to_scan\ 23 | (domain TEXT, ip_rowid INTEGER, UNIQUE(domain, ip_rowid))" 24 | 25 | ips_to_scan = "CREATE TABLE ips_to_scan\ 26 | (ip TEXT, version INTEGER, UNIQUE(ip, version))" 27 | 28 | cidrs_to_scan = "CREATE TABLE cidrs_to_scan\ 29 | (cidr TEXT, version, UNIQUE(cidr, version))" 30 | 31 | scan_results = "CREATE TABLE scan_results\ 32 | (ip TEXT, port INTEGER, protocol TEXT, UNIQUE(ip, port, protocol))" 33 | 34 | cur.execute(domains_to_scan) 35 | cur.execute(ips_to_scan) 36 | cur.execute(cidrs_to_scan) 37 | cur.execute(scan_results) 38 | 39 | self.conn.commit() 40 | 41 | def report_port(self, ip, port, protocol): 42 | """Reports new port to the database 43 | 44 | Args: 45 | ip (str): IPv4 or IPv6 address 46 | port (int): port number 47 | protocol (str): tcp or udp 48 | """ 49 | conn = self.conn 50 | cur = conn.cursor() 51 | 52 | cur.execute( 53 | "INSERT OR IGNORE INTO\ 54 | scan_results(ip, port, protocol)\ 55 | VALUES(?, ?, ?)", 56 | (ip, port, protocol), 57 | ) 58 | 59 | conn.commit() 60 | 61 | def add_cidr(self, cidr): 62 | """Adds CIDR to the database 63 | 64 | Args: 65 | cidr (str): IPv6 or IPv4 CIDR 66 | """ 67 | logger = self.logger 68 | conn = self.conn 69 | scope_file = self.scope_file 70 | cur = conn.cursor() 71 | 72 | # Check if the CIDR is in scope 73 | if scope_file is not None: 74 | if self.target_in_scope(cidr) is False: 75 | logger.warning("%s is out of scope, skipping...", cidr) 76 | return 77 | 78 | cur.execute( 79 | "INSERT OR IGNORE INTO cidrs_to_scan(cidr, version) VALUES(?, ?)", (cidr, 4) 80 | ) 81 | 82 | conn.commit() 83 | 84 | def add_domain(self, domain): 85 | """Adds domain to the database 86 | 87 | Args: 88 | domain (str): domain name 89 | """ 90 | utils = self.utils 91 | ips = utils.resolve_host(domain) 92 | 93 | conn = self.conn 94 | cur = conn.cursor() 95 | 96 | if ips is None or len(ips) == 0: 97 | query = "INSERT OR IGNORE INTO domains_to_scan(domain) VALUES(?)" 98 | cur.execute(query, (domain,)) 99 | conn.commit() 100 | 101 | return 102 | 103 | for ip in ips: 104 | self.add_ip(ip) 105 | 106 | cur.execute( 107 | "INSERT OR IGNORE INTO\ 108 | domains_to_scan(domain, ip_rowid) \ 109 | VALUES(?, (SELECT rowid FROM ips_to_scan where ip = ?))", 110 | (domain, ip), 111 | ) 112 | 113 | conn.commit() 114 | 115 | def add_ip(self, ip): 116 | """Adds IP to database 117 | 118 | Args: 119 | ip (str): IPv4 or IPv6 120 | """ 121 | logger = self.logger 122 | conn = self.conn 123 | scope_file = self.scope_file 124 | cur = conn.cursor() 125 | 126 | # Check if the IP is in scope before adding it to the database 127 | if scope_file is not None: 128 | if self.target_in_scope(ip) is False: 129 | logger.warning("%s is out of scope, skipping...", ip) 130 | return 131 | 132 | query = "INSERT OR IGNORE INTO ips_to_scan(ip, version) VALUES(?, ?)" 133 | 134 | if Validator.is_ipv4(ip): 135 | cur.execute(query, (ip, 4)) 136 | elif Validator.is_ipv6(ip): 137 | cur.execute(query, (ip, 6)) 138 | else: 139 | logger.warning("%s is not an valid IPv4 or IPv6 address, not scanning", ip) 140 | 141 | conn.commit() 142 | 143 | def get_ips(self): 144 | """Gets all IPs from database 145 | 146 | Returns: 147 | list: list of IP tuples 148 | """ 149 | conn = self.conn 150 | cur = conn.cursor() 151 | 152 | ips = cur.execute("SELECT DISTINCT ip FROM ips_to_scan").fetchall() 153 | 154 | return ips 155 | 156 | def get_results_complex(self): 157 | """Gets results in complex format 158 | 159 | Returns: 160 | list: Returns list of lists such is [domain.com, domain-alternative.com], 1.1.1.1, [80, 443] 161 | """ 162 | conn = self.conn 163 | cur = conn.cursor() 164 | ips = cur.execute("SELECT DISTINCT ip FROM scan_results").fetchall() 165 | results = [] 166 | for ip, in ips: 167 | ports = cur.execute("SELECT DISTINCT port FROM scan_results WHERE ip = ?", (ip,)).fetchall() 168 | domains = cur.execute( 169 | "SELECT domain FROM domains_to_scan\ 170 | WHERE ip_rowid = (SELECT rowid FROM ips_to_scan WHERE ip = ?)", 171 | (ip,), 172 | ).fetchall() 173 | 174 | if len(domains) != 0: 175 | results.append( 176 | ([domain for domain, in domains], ip, [port for port, in ports]) 177 | ) 178 | else: 179 | results.append(([], ip, [port for port, in ports])) 180 | 181 | return results 182 | 183 | 184 | def get_cidrs(self): 185 | """Gets all CIDRs from the database 186 | 187 | Returns: 188 | list: Returns list of cidrs in tuple format 189 | """ 190 | conn = self.conn 191 | cur = conn.cursor() 192 | 193 | cidrs = cur.execute("SELECT DISTINCT cidr FROM cidrs_to_scan").fetchall() 194 | 195 | return cidrs 196 | 197 | def get_scan_results(self): 198 | """Generates scan results in format target:port 199 | 200 | Args: 201 | ips (bool, optional): True to show IP:port. Defaults to False. 202 | domains (bool, optional): True to show domain:port. Defaults to False. 203 | 204 | Returns: 205 | list: Returns list in (domain|ip):port format 206 | """ 207 | conn = self.conn 208 | cur = conn.cursor() 209 | 210 | ips = [] 211 | domains = [] 212 | 213 | rows = cur.execute( 214 | "SELECT DISTINCT ip, port FROM scan_results" 215 | ).fetchall() 216 | for row in rows: 217 | ips.append(f"{row[0]}:{row[1]}") 218 | 219 | 220 | rows = cur.execute( 221 | "SELECT DISTINCT domain, ip, port FROM scan_results\ 222 | JOIN domains_to_scan ON domain = domains_to_scan.domain WHERE domains_to_scan.ip_rowid = (SELECT rowid FROM ips_to_scan WHERE ip = scan_results.ip) ORDER BY domain" 223 | ).fetchall() 224 | 225 | for row in rows: 226 | domains.append(f"{row[0]}:{row[2]}") 227 | 228 | ips_unique = list(set(ips)) 229 | domains_unique = list(set(domains)) 230 | 231 | ips_unique.sort() 232 | domains_unique.sort() 233 | 234 | return ips_unique, domains_unique 235 | 236 | def count_ips(self): 237 | """Get number of all IPs to scan, including IPs in network ranges 238 | 239 | Returns: 240 | int: Number of IPs in database, including CIDRS 241 | """ 242 | conn = self.conn 243 | cur = conn.cursor() 244 | logger = self.logger 245 | 246 | cidrs = cur.execute("SELECT DISTINCT cidr FROM cidrs_to_scan").fetchall() 247 | 248 | address_count = 0 249 | 250 | for (cidr,) in cidrs: 251 | if Validator.is_ipv6_cidr(cidr): 252 | address_count += (2 ** (128 - int(cidr.split("/")[1]))) 253 | elif Validator.is_ipv4_cidr(cidr): 254 | address_count += (2 ** (32 - int(cidr.split("/")[1]))) 255 | 256 | ips_count = cur.execute("SELECT count(DISTINCT ip) FROM ips_to_scan").fetchall() 257 | 258 | address_count += ips_count[0][0] 259 | 260 | return address_count 261 | 262 | def count_ports(self): 263 | """Gets number of discovered ports 264 | 265 | Returns: 266 | int: Number of ports 267 | """ 268 | conn = self.conn 269 | cur = conn.cursor() 270 | 271 | port_count = cur.execute("SELECT count(*) FROM scan_results").fetchall() 272 | 273 | return port_count[0][0] 274 | 275 | def count_alive_ips(self): 276 | """Gets number of IPs that are "alive" - judging by the open ports 277 | 278 | Returns: 279 | int: Number of IPs alive 280 | """ 281 | conn = self.conn 282 | cur = conn.cursor() 283 | 284 | port_count = cur.execute("SELECT count(DISTINCT ip) FROM scan_results").fetchall() 285 | 286 | return port_count[0][0] 287 | 288 | def target_in_scope(self, target): 289 | """Function to check if IP or CIDR is in scope (loaded from scope file) 290 | 291 | Args: 292 | ip (str): IP address or CIDR 293 | 294 | Returns: 295 | bool: Returns True if IP/CIDR is in scope, False if not 296 | """ 297 | 298 | logger = self.logger 299 | file_path = self.scope_file 300 | utils = self.utils 301 | 302 | if self.utils.file_is_empty(file_path): 303 | logger.fatal( 304 | "scope file is empty or does not exists: %s", 305 | file_path, 306 | ) 307 | raise SystemExit(1) 308 | 309 | with open(file_path, "r", encoding='UTF-8') as scope: 310 | for scope_item in scope.readlines(): 311 | 312 | scope_item = scope_item.strip() 313 | 314 | # If scope item is just IP 315 | if target == scope_item: 316 | return True 317 | 318 | # If scope item is in CIDR notation 319 | elif Validator.is_ipv6_cidr(scope_item): 320 | 321 | # If checked target is just IP 322 | if Validator.is_ipv6(target): 323 | # We just ask if the target is part of network 324 | if ipaddress.ip_address(target) in ipaddress.ip_network(scope_item): 325 | return True 326 | 327 | # If checked target is in CIDR notation 328 | if Validator.is_ipv6_cidr(target): 329 | # We ask if subnet is part of network 330 | network = ipaddress.ip_network(scope_item) 331 | if network.supernet_of(ipaddress.ip_network(target)) is True: 332 | return True 333 | 334 | # If scope item is in CIDR notation 335 | elif Validator.is_ipv4_cidr(scope_item): 336 | # If checked target is just IP 337 | if Validator.is_ipv4(target): 338 | # We just ask if the target is part of network 339 | if ipaddress.ip_address(target) in ipaddress.ip_network(scope_item): 340 | return True 341 | 342 | # If checked target is in CIDR notation 343 | if Validator.is_ipv4_cidr(target): 344 | # We ask if subnet is part of network 345 | network = ipaddress.ip_network(scope_item) 346 | if network.supernet_of(ipaddress.ip_network(target)) is True: 347 | return True 348 | 349 | elif Validator.is_domain(scope_item) is True: 350 | resolved_scope_item = utils.resolve_host(scope_item) 351 | if resolved_scope_item is not None: 352 | if target in resolved_scope_item: 353 | return True 354 | 355 | # By default, we want to return False 356 | return False 357 | -------------------------------------------------------------------------------- /jfscan/core/utils.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=import-error 2 | #!/usr/bin/env python3 3 | import subprocess 4 | import logging 5 | import os 6 | import sys 7 | import socket 8 | import random 9 | import string 10 | import selectors 11 | import dns.resolver 12 | 13 | from jfscan.core.validator import Validator 14 | 15 | class Utils: 16 | def __init__(self): 17 | self.logger = logging.getLogger(__name__) 18 | self.resolvers = None 19 | self.enable_ipv6 = False 20 | 21 | def check_dependency(self, binary, version_flag=None, version_string=None): 22 | logger = self.logger 23 | 24 | result = subprocess.run( 25 | f"which {binary}", 26 | capture_output=True, 27 | shell=True, 28 | check=False, 29 | ) 30 | 31 | if result.returncode == 1: 32 | logger.fatal("%s is not installed", binary) 33 | 34 | raise SystemExit(1) 35 | 36 | if version_flag and version_string is not None: 37 | result = subprocess.run( 38 | f"{binary} {version_flag}", 39 | capture_output=True, 40 | shell=True, 41 | check=False, 42 | ) 43 | 44 | if version_string not in str(result.stdout): 45 | logger.fatal( 46 | "wrong version of %s is installed - version %s is required", 47 | binary, 48 | version_string, 49 | ) 50 | 51 | raise SystemExit(1) 52 | 53 | def handle_command(self, cmd, stream_output=False): 54 | logger = self.logger 55 | 56 | logger.debug("running command %s", cmd) 57 | 58 | _stdout = b"" 59 | _stderr = b"" 60 | 61 | try: 62 | with subprocess.Popen( 63 | cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE 64 | ) as process: 65 | sel = selectors.DefaultSelector() 66 | sel.register(process.stdout, selectors.EVENT_READ) 67 | sel.register(process.stderr, selectors.EVENT_READ) 68 | 69 | while True: 70 | for key, _ in sel.select(): 71 | data = key.fileobj.read1() 72 | if not data: 73 | process.wait() 74 | returncode = process.poll() 75 | if returncode != 0: 76 | logger.error( 77 | "there was an exception while running command:\n %s", 78 | cmd, 79 | ) 80 | return subprocess.CompletedProcess( 81 | process.args, process.returncode, _stdout, _stderr 82 | ) 83 | if key.fileobj is process.stdout: 84 | if stream_output is True: 85 | print(data.decode(), end="") 86 | _stdout += data 87 | else: 88 | if stream_output is True: 89 | print(data.decode(), end="", file=sys.stderr) 90 | _stderr += data 91 | except KeyboardInterrupt: 92 | logger.error( 93 | "process was killed, continuing..." 94 | ) 95 | process.kill() 96 | return subprocess.CompletedProcess( 97 | process.args, process.returncode, _stdout, _stderr 98 | ) 99 | 100 | def resolve_host(self, host): 101 | logger = self.logger 102 | 103 | resolver = dns.resolver.Resolver() 104 | 105 | if self.resolvers is not None: 106 | resolver.nameservers = self.resolvers 107 | 108 | ips = [] 109 | 110 | if self.enable_ipv6 is True: 111 | queries = ["A", "AAAA"] 112 | else: 113 | queries = ["A"] 114 | 115 | for query in queries: 116 | try: 117 | result = resolver.query(host, query) 118 | except Exception as e: 119 | logger.debug( 120 | "%s could not be resolved by provided resolvers (%s):\n %s", host, query, e 121 | ) 122 | result = None 123 | 124 | if result is not None and len(result) != 0: 125 | for ipval in result: 126 | ips.append(ipval.to_text()) 127 | 128 | if len(ips) == 0: 129 | logger.warning("host %s could not be resolved", host) 130 | return None 131 | 132 | ips = list(set(ips)) 133 | 134 | logger.debug("host %s was resolved to: %s", host, ", ".join(ips)) 135 | 136 | return ips 137 | 138 | """ 139 | Beta feature: Not tested, maybe it's not working as intended. 140 | """ 141 | 142 | def detect_firewall(self, host): 143 | random_ports = random.sample(range(50000, 65535), 90) 144 | open_ports = [] 145 | 146 | for port in random_ports: 147 | if self.is_port_open(host, port): 148 | open_ports.append(port) 149 | 150 | if len(open_ports) > len(random_ports) / 10: 151 | return True 152 | 153 | return False 154 | 155 | @staticmethod 156 | def is_port_open(host, port): 157 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 158 | sock.settimeout(1) 159 | try: 160 | result = sock.connect_ex((host, port)) 161 | except: 162 | pass 163 | 164 | return bool(result) 165 | 166 | def save_results(self, results, target_file): 167 | logger = self.logger 168 | try: 169 | with open(target_file, "w", encoding="UTF-8") as f: 170 | for result in results: 171 | f.write(f"{result}\n") 172 | except Exception as e: 173 | logger.error("Could not save results to the specified file %s:\n %s", target_file, e) 174 | 175 | def load_targets(self, res, targets_file=None, target=None): 176 | logger = self.logger 177 | targets = [] 178 | 179 | logger.info("loading targets and resolving domain names (if any)") 180 | 181 | if targets_file is not None: 182 | if self.file_is_empty(targets_file): 183 | logger.fatal( 184 | "input file is empty or does not exists: %s", 185 | targets_file, 186 | ) 187 | raise SystemExit(1) 188 | 189 | with open(targets_file, "r", encoding="UTF-8") as _file: 190 | targets += _file.readlines() 191 | 192 | if target is not None: 193 | targets += target 194 | 195 | if sys.stdin.isatty() is False: 196 | logger.info("reading input from stdin") 197 | targets += sys.stdin.readlines() 198 | 199 | if len(targets) == 0: 200 | logger.error("no valid targets were specified") 201 | raise SystemExit(1) 202 | 203 | 204 | for _target in list(set(targets)): 205 | 206 | _target = _target.strip() 207 | 208 | # Domain from URL must be extracted first 209 | if Validator.is_url(_target): 210 | _target = _target.split("/")[2] 211 | 212 | if Validator.is_domain(_target): 213 | res.add_domain(_target) 214 | 215 | elif Validator.is_ipv4(_target) or Validator.is_ipv6(_target): 216 | res.add_ip(_target) 217 | 218 | elif Validator.is_ipv4_cidr(_target) or Validator.is_ipv6_cidr(_target): 219 | res.add_cidr(_target) 220 | 221 | elif Validator.is_ipv4_range(_target): 222 | cidrs = self.ipv4_range_to_cidrs(_target) 223 | 224 | logger.debug("IP range %s was divided into the following CIDRs: %s", _target, ", ".join(cidrs)) 225 | 226 | for cidr in cidrs: 227 | res.add_cidr(cidr) 228 | 229 | elif Validator.is_ipv6_range(_target): 230 | cidrs = self.ipv6_range_to_cidrs(_target) 231 | 232 | logger.debug("IP range %s was divided into the following CIDRs: %s", _target, ", ".join(cidrs)) 233 | 234 | for cidr in cidrs: 235 | res.add_cidr(cidr) 236 | 237 | else: 238 | logger.warning("host %s is in unrecognized format, skipping...", _target) 239 | 240 | @staticmethod 241 | def ipv4_range_to_cidrs(ip_range): 242 | """Converts target specified as IP range (inetnum) to CIDR(s) 243 | 244 | Args: 245 | ip_range (str): IP range - 192.168.0.0-192.168.1.255 246 | 247 | Returns: 248 | list: list of CIDR(s) 249 | """ 250 | import ipaddress 251 | try: 252 | ip_range = ip_range.split("-") 253 | startip = ipaddress.IPv4Address(ip_range[0]) 254 | endip = ipaddress.IPv4Address(ip_range[1]) 255 | return [str(ipaddr) for ipaddr in ipaddress.summarize_address_range(startip, endip)] 256 | except: 257 | return None 258 | 259 | @staticmethod 260 | def ipv6_range_to_cidrs(ip_range): 261 | """Converts target specified as IP range (inetnum) to CIDR(s) 262 | 263 | Args: 264 | ip_range (str): IP range - 2620:0:2d0:200::7-2620:0:2d0:2df::7 265 | 266 | Returns: 267 | list: list of CIDR(s) 268 | """ 269 | import ipaddress 270 | try: 271 | ip_range = ip_range.split("-") 272 | startip = ipaddress.IPv6Address(ip_range[0]) 273 | endip = ipaddress.IPv6Address(ip_range[1]) 274 | return [str(ipaddr) for ipaddr in ipaddress.summarize_address_range(startip, endip)] 275 | except: 276 | return None 277 | 278 | # Oh, just remove it already... 279 | @staticmethod 280 | def file_is_empty(file): 281 | try: 282 | if os.path.exists(file) is not True or os.path.getsize(file) == 0: 283 | return True 284 | except: 285 | return True 286 | else: 287 | return False 288 | 289 | @staticmethod 290 | def random_string(): 291 | return "".join( 292 | random.choice(string.ascii_lowercase + string.digits) for _ in range(9) 293 | ) 294 | 295 | @staticmethod 296 | def yummy_ports(): 297 | return [ 298 | 10000, 299 | 10006, 300 | 10009, 301 | 10010, 302 | 10026, 303 | 10037, 304 | 10047, 305 | 10048, 306 | 10080, 307 | 10087, 308 | 10089, 309 | 10093, 310 | 10100, 311 | 10136, 312 | 10141, 313 | 10187, 314 | 1022, 315 | 10256, 316 | 1026, 317 | 10283, 318 | 10443, 319 | 10477, 320 | 1050, 321 | 10543, 322 | 10652, 323 | 10691, 324 | 10776, 325 | 1080, 326 | 1099, 327 | 111, 328 | 1110, 329 | 11180, 330 | 11680, 331 | 1194, 332 | 12088, 333 | 12170, 334 | 12200, 335 | 12211, 336 | 12283, 337 | 12318, 338 | 12320, 339 | 12323, 340 | 12325, 341 | 12327, 342 | 12378, 343 | 12383, 344 | 12424, 345 | 12432, 346 | 12437, 347 | 12457, 348 | 12490, 349 | 12516, 350 | 12584, 351 | 12588, 352 | 1343, 353 | 135, 354 | 139, 355 | 1433, 356 | 1444, 357 | 15672, 358 | 15673, 359 | 16004, 360 | 16017, 361 | 16029, 362 | 16036, 363 | 16052, 364 | 16059, 365 | 16063, 366 | 16082, 367 | 161, 368 | 16100, 369 | 16316, 370 | 16443, 371 | 16992, 372 | 18001, 373 | 18042, 374 | 18094, 375 | 18888, 376 | 19015, 377 | 19082, 378 | 19999, 379 | 20000, 380 | 20010, 381 | 2002, 382 | 2030, 383 | 2049, 384 | 20512, 385 | 2052, 386 | 2053, 387 | 2063, 388 | 2078, 389 | 2079, 390 | 2082, 391 | 2083, 392 | 2086, 393 | 2087, 394 | 2096, 395 | 21, 396 | 2100, 397 | 2103, 398 | 2107, 399 | 2108, 400 | 2109, 401 | 2111, 402 | 2121, 403 | 2122, 404 | 2123, 405 | 2126, 406 | 21299, 407 | 2130, 408 | 2133, 409 | 2134, 410 | 2156, 411 | 2195, 412 | 2196, 413 | 22, 414 | 2200, 415 | 22206, 416 | 23, 417 | 2301, 418 | 2323, 419 | 2375, 420 | 2377, 421 | 2381, 422 | 2443, 423 | 2455, 424 | 25000, 425 | 2570, 426 | 2598, 427 | 27017, 428 | 27018, 429 | 27019, 430 | 3000, 431 | 30000, 432 | 3001, 433 | 3002, 434 | 30027, 435 | 3003, 436 | 3004, 437 | 3005, 438 | 3006, 439 | 3007, 440 | 3008, 441 | 3009, 442 | 30113, 443 | 30452, 444 | 3048, 445 | 3081, 446 | 3100, 447 | 3111, 448 | 3120, 449 | 3121, 450 | 3128, 451 | 3175, 452 | 3190, 453 | 31948, 454 | 3199, 455 | 3200, 456 | 32102, 457 | 32444, 458 | 3306, 459 | 3322, 460 | 3343, 461 | 3443, 462 | 3551, 463 | 35531, 464 | 3580, 465 | 3582, 466 | 389, 467 | 40000, 468 | 40005, 469 | 4040, 470 | 4045, 471 | 4101, 472 | 4165, 473 | 42420, 474 | 443, 475 | 4431, 476 | 4432, 477 | 4433, 478 | 444, 479 | 4443, 480 | 4444, 481 | 44443, 482 | 44444, 483 | 445, 484 | 4510, 485 | 4560, 486 | 45886, 487 | 47001, 488 | 4712, 489 | 4848, 490 | 49443, 491 | 49682, 492 | 49694, 493 | 5000, 494 | 50001, 495 | 50002, 496 | 5001, 497 | 5004, 498 | 50080, 499 | 50202, 500 | 5022, 501 | 5044, 502 | 5060, 503 | 5061, 504 | 5080, 505 | 5090, 506 | 520, 507 | 5236, 508 | 5252, 509 | 5272, 510 | 5357, 511 | 5400, 512 | 5432, 513 | 5443, 514 | 5500, 515 | 5555, 516 | 556, 517 | 5601, 518 | 5671, 519 | 5672, 520 | 5673, 521 | 5701, 522 | 5900, 523 | 5901, 524 | 5911, 525 | 5984, 526 | 5985, 527 | 5989, 528 | 60000, 529 | 6066, 530 | 6070, 531 | 632, 532 | 636, 533 | 6379, 534 | 6666, 535 | 6688, 536 | 7000, 537 | 7070, 538 | 7077, 539 | 7080, 540 | 7332, 541 | 7403, 542 | 7424, 543 | 7443, 544 | 7445, 545 | 7446, 546 | 7547, 547 | 7672, 548 | 7776, 549 | 7777, 550 | 7914, 551 | 7946, 552 | 7990, 553 | 7991, 554 | 7992, 555 | 7993, 556 | 7999, 557 | 80, 558 | 8000, 559 | 8001, 560 | 8002, 561 | 8003, 562 | 8007, 563 | 8008, 564 | 8009, 565 | 8012, 566 | 8022, 567 | 8043, 568 | 805, 569 | 8060, 570 | 8080, 571 | 8081, 572 | 8082, 573 | 8083, 574 | 8084, 575 | 8085, 576 | 8086, 577 | 8088, 578 | 8089, 579 | 8090, 580 | 8091, 581 | 8095, 582 | 8098, 583 | 81, 584 | 8100, 585 | 8101, 586 | 8120, 587 | 8123, 588 | 8137, 589 | 8150, 590 | 8152, 591 | 8161, 592 | 8187, 593 | 82, 594 | 8200, 595 | 83, 596 | 8381, 597 | 8403, 598 | 8411, 599 | 8443, 600 | 8454, 601 | 8519, 602 | 8550, 603 | 8573, 604 | 8634, 605 | 8707, 606 | 880, 607 | 8810, 608 | 8831, 609 | 8834, 610 | 8843, 611 | 8844, 612 | 8855, 613 | 8866, 614 | 8880, 615 | 8888, 616 | 8899, 617 | 8983, 618 | 8989, 619 | 9000, 620 | 9001, 621 | 9010, 622 | 9016, 623 | 9024, 624 | 9033, 625 | 9080, 626 | 9081, 627 | 9084, 628 | 9088, 629 | 9090, 630 | 9091, 631 | 9098, 632 | 9100, 633 | 9114, 634 | 9115, 635 | 9116, 636 | 9120, 637 | 9121, 638 | 9153, 639 | 9162, 640 | 9200, 641 | 9207, 642 | 9208, 643 | 9214, 644 | 9256, 645 | 9300, 646 | 9306, 647 | 9443, 648 | 9600, 649 | 9696, 650 | 9700, 651 | 9882, 652 | 9901, 653 | 9928, 654 | 9966, 655 | 9990, 656 | 9998, 657 | 9999 658 | ] 659 | 660 | @staticmethod 661 | def compute_rate(num_ips, num_ports, max_rate): 662 | computed_rate = num_ips * num_ports / (num_ports / 100) 663 | 664 | if computed_rate > max_rate: 665 | return int(max_rate) 666 | 667 | return int(computed_rate) 668 | --------------------------------------------------------------------------------