Skip to content

Commit

Permalink
Merge pull request #12 from PaloAltoNetworks/timeout-and-errors
Browse files Browse the repository at this point in the history
Error handling cleanup
  • Loading branch information
andrew-paloalto authored Feb 9, 2021
2 parents a83c13b + 09d982e commit ecf16e1
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 26 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this this project will be documented in this file.
The format is based on [changelog.md](https://changelog.md/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.5] - 2021-02-08
- Add timeout and swallow error command line options
- Ensure that unsuccessful scan still return a valid JARM fingerprint (00000000000000000000000000000000000000000000000000000000000000)

## [0.0.4] - 2021-01-28
- Add improved command line support

Expand Down
43 changes: 29 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ pip install pyjarm

### Command Line
```
usage: pyjarm [-h] [-i INPUT] [-d] [-o OUTPUT] [-4] [-6] [-c [CONCURRENCY]] [--proxy PROXY]
[--proxy-auth PROXY_AUTH] [--proxy-insecure]
[scan]
usage: jarm [-h] [-i INPUT] [-d] [-o OUTPUT] [-4] [-6] [-c [CONCURRENCY]]
[--proxy PROXY] [--proxy-auth PROXY_AUTH] [--proxy-insecure]
[--timeout TIMEOUT] [--suppress]
[scan]
Enter an IP address/domain and port to scan or supply an input file.
Expand All @@ -33,20 +34,34 @@ positional arguments:
optional arguments:
-h, --help show this help message and exit
-i INPUT, --input INPUT
Provide a list of IP addresses or domains to scan, one domain or IP address per line. Ports
can be specified with a colon (ex. 8.8.8.8:8443)
-d, --debug [OPTIONAL] Debug mode: Displays additional debug details
Provide a list of IP addresses or domains to scan, one
domain or IP address per line. Ports can be specified
with a colon (ex. 8.8.8.8:8443)
-d, --debug [OPTIONAL] Debug mode: Displays additional debug
details
-o OUTPUT, --output OUTPUT
[OPTIONAL] Provide a filename to output/append results to a CSV file.
-4, --ipv4only [OPTIONAL] Use only IPv4 connections (incompatible with --ipv6only).
-6, --ipv6only [OPTIONAL] Use only IPv6 connections (incompatible with --ipv4only).
[OPTIONAL] Provide a filename to output/append results
to a CSV file.
-4, --ipv4only [OPTIONAL] Use only IPv4 connections (incompatible
with --ipv6only).
-6, --ipv6only [OPTIONAL] Use only IPv6 connections (incompatible
with --ipv4only).
-c [CONCURRENCY], --concurrency [CONCURRENCY]
[OPTIONAL] Number of concurrent connections (default is 2).
--proxy PROXY [OPTIONAL] Use proxy (format http[s]://user:pass@proxy:port). HTTPS_PROXY env variable is used
by default if this is not set. Set this to 'ignore' to ignore HTTPS_PROXY and use no proxy.
[OPTIONAL] Number of concurrent connections (default
is 2).
--proxy PROXY [OPTIONAL] Use proxy (format
http[s]://user:pass@proxy:port). HTTPS_PROXY env
variable is used by default if this is not set. Set
this to 'ignore' to ignore HTTPS_PROXY and use no
proxy.
--proxy-auth PROXY_AUTH
[OPTIONAL] Send this header in Proxy-Authorization (when using proxy).
--proxy-insecure [OPTIONAL] Do not verify SSL_CERTIFICATES (only when HTTPS proxy is set).
[OPTIONAL] Send this header in Proxy-Authorization
(when using proxy).
--proxy-insecure [OPTIONAL] Do not verify SSL_CERTIFICATES (only when
HTTPS proxy is set).
--timeout TIMEOUT [OPTIONAL] Timeout to wait for connection attempts.
Default is 20 seconds
--suppress [OPTIONAL] Suppresses any exception or warning logging.
```

**Example**
Expand Down
2 changes: 1 addition & 1 deletion jarm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__author__ = "Andrew Scott"
__version__ = "0.0.4"
__version__ = "0.0.5"
__license__ = "ISC"
__status__ = "Development"
26 changes: 23 additions & 3 deletions jarm/cli.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import argparse
from datetime import datetime, timezone
import asyncio
from contextlib import suppress
from datetime import datetime, timezone
import logging

try:
from jarm.constants import DEFAULT_TIMEOUT
from jarm.scanner.scanner import Scanner
from jarm.connection.connection import Connection
except ImportError:
import os
import sys

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from jarm.constants import DEFAULT_TIMEOUT
from jarm.scanner.scanner import Scanner
from jarm.connection.connection import Connection

Expand All @@ -21,6 +25,8 @@ def _scan(
proxy_auth: str = None,
proxy_insecure: bool = None,
concurrency: int = 2,
timeout: int = DEFAULT_TIMEOUT,
suppress: bool = False,
):
if ":" in target:
parts = target.split(":")
Expand All @@ -34,11 +40,13 @@ def _scan(
Scanner.scan_async(
dest_host=host,
dest_port=port,
timeout=timeout,
address_family=address_family,
proxy=proxy,
proxy_auth=proxy_auth,
proxy_insecure=proxy_insecure,
concurrency=concurrency,
suppress=suppress,
)
)
print(f"JARM: {results[0]}")
Expand Down Expand Up @@ -103,11 +111,19 @@ def run():
help="[OPTIONAL] Do not verify SSL_CERTIFICATES (only when HTTPS proxy is set).",
action="store_true",
)
parser.add_argument(
"--timeout",
help="[OPTIONAL] Timeout to wait for connection attempts. Default is 20 seconds",
type=int,
)
parser.add_argument(
"--suppress",
help="[OPTIONAL] Suppresses any exception logging.",
action="store_true",
)
args = parser.parse_args()
concurrency = args.concurrency if args.concurrency else 2
if args.debug:
import logging

logging.basicConfig(level=logging.DEBUG)
if args.ipv4only and args.ipv6only:
parser.error("Cannot specify both --ipv4only and --ipv6only at the same time")
Expand All @@ -127,6 +143,8 @@ def run():
proxy_auth=args.proxy_auth,
proxy_insecure=args.proxy_insecure,
concurrency=concurrency,
timeout=args.timeout,
suppress=args.suppress,
)
]
else:
Expand All @@ -143,6 +161,8 @@ def run():
proxy_auth=args.proxy_auth,
proxy_insecure=args.proxy_insecure,
concurrency=concurrency,
timeout=args.timeout,
suppress=args.suppress,
)
)
if args.output is not None:
Expand Down
6 changes: 2 additions & 4 deletions jarm/connection/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
from enum import IntEnum
import ssl

from jarm.constants import DEFAULT_TIMEOUT
from jarm.proxy.proxy import Proxy
from jarm.exceptions.exceptions import PyJARMInvalidProxy
from jarm.validate.validate import Validate


class Connection:

DEFAULT_TIMEOUT = 20 # default timeout in seconds

class AddressFamily(IntEnum):
AF_ANY = 0
AF_INET = 2
Expand Down Expand Up @@ -65,7 +63,7 @@ async def jarm_connect(

timeout = connect_args.get("timeout")
if not timeout or not isinstance(timeout, int):
timeout = Connection.DEFAULT_TIMEOUT
timeout = DEFAULT_TIMEOUT

proxy_string = connect_args.get("proxy")
if proxy_string and not isinstance(proxy_string, str):
Expand Down
3 changes: 3 additions & 0 deletions jarm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@
FAILED_PACKET: str = "|||"
ERROR_INC_1: bytes = b"\x0e\xac\x0b"
ERROR_INC_2: bytes = b"\x0f\xf0\x0b"

# CONNECTION
DEFAULT_TIMEOUT = 20
17 changes: 13 additions & 4 deletions jarm/scanner/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import asyncio
from typing import List, Any
import warnings

from jarm.constants import TOTAL_FAILURE, FAILED_PACKET, ERROR_INC_1, ERROR_INC_2
from jarm.formats import V1
Expand Down Expand Up @@ -41,6 +42,7 @@ async def scan_async(
proxy_auth: str = None,
proxy_insecure: bool = None,
concurrency: int = 2,
suppress: bool = False,
):
"""
Kicks off a number of TLS hello packets to a server then parses and hashes the response.
Expand All @@ -50,7 +52,7 @@ async def scan_async(
The target host. This can be an IPv4 address, IPv6 address, or domain name.
dest_port (int):
The target port.
timeout (int, optional):
timeout (int, optional, default=20):
How long to wait for the server to response. Default is 20 seconds.
address_family (int, optional):
The address family for the scan. This will default to ANY and is used to validate the target.
Expand All @@ -64,8 +66,10 @@ async def scan_async(
proxy_insecure (bool, optional):
If you are connecting through an HTTPS proxy via TLS, this flag disables the verification of the proxy
certificate.
concurrency (int, optional, default=2)
concurrency (int, optional, default=2):
Number of concurrent TCP connections to generate.
suppress (bool, optional, default=False):
Suppresses any raised exceptions encountered during scanning
Returns:
:tuple:
Returns a tuple with three items. The first item is the JARM hash, which is a string. Second is
Expand All @@ -83,7 +87,11 @@ async def scan_async(
"proxy": proxy,
"proxy_auth": proxy_auth,
"verify": False if proxy_insecure else True,
"timeout": timeout,
}

if suppress:
warnings.filterwarnings("ignore")
packet_tuples = Scanner._generate_packets(
dest_host=dest_host, dest_port=dest_port
)
Expand All @@ -100,8 +108,9 @@ async def scan_async(
if p[0] == r[0]:
results.append(Scanner._parse_server_hello(r[1], p))
except Exception:
logging.exception(f"Unknown Exception scanning {target}")
return TOTAL_FAILURE, target.host, target.port
if not suppress:
logging.exception(f"Unknown Exception scanning {target}")
return Hasher.jarm(TOTAL_FAILURE), target.host, target.port
return Hasher.jarm(",".join(results)), target.host, target.port

@staticmethod
Expand Down

0 comments on commit ecf16e1

Please sign in to comment.