Metadata-Version: 2.5
Name: pyzgrab2
Version: 1.1.0
Summary: A Python interface for the zgrab2 application-layer scanner
Project-URL: Homepage, https://gitlab.com/dgiakatos/pyzgrab2
Project-URL: Repository, https://gitlab.com/dgiakatos/pyzgrab2
Project-URL: Issues, https://gitlab.com/dgiakatos/pyzgrab2/-/issues
Author: Dimitrios Giakatos
License-Expression: GPL-3.0
License-File: LICENSE
Keywords: internet-measurement,network-scanning,security,zgrab2
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# pyzgrab2

`pyzgrab2` is a typed Python wrapper for the
[ZGrab 2.0](https://github.com/zmap/zgrab2) application-layer scanner. It
streams targets to a local `zgrab2` process and converts its JSON-lines output into Python objects.

pyzgrab2 provides convenient option models for a variety of protocols:

<table>
<tr><td>HTTP</td><td>TLS</td><td>SSH</td><td>IMAP</td><td>FTP</td><td>SMTP</td></tr>
</table>

Every other zgrab2 module remains available through the generic `scan()` API.

> Only scan systems you own or have explicit permission to test. You are
> responsible for the traffic generated by zgrab2 and for complying with
> applicable policies and laws.

## Requirements

- Python 3.10 or newer
- A working `zgrab2` executable

Install zgrab2 by following its
[official installation instructions](https://github.com/zmap/zgrab2#installation),
then confirm that it is on your `PATH`:

```sh
zgrab2 --help
```

You can also give `zgrab2` an explicit path to the binary.

## Installation

From a package index:

```sh
pip install pyzgrab2
```

### Development installation

Clone the repository, create and activate a virtual environment, and install
pyzgrab2 in editable mode with its development tools:

```sh
git clone https://gitlab.com/dgiakatos/pyzgrab2.git
cd pyzgrab2
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -e ".[dev]"
```

Run the checks with:

```sh
pytest
ruff check src tests
mypy src tests
```

## Quick start

```python
from pyzgrab2 import HTTPOptions, ScanOptions, Target, ZGrab2

scanner = ZGrab2()

targets = [
    'example.com',
    Target(ip='192.0.2.10', domain='service.example.com', tag='web'),
]

results = scanner.http(
    targets,
    http=HTTPOptions(
        use_https=True,
        endpoint='/',
        max_redirects=2,
    ),
    options=ScanOptions(port=443, senders=100),
    timeout=120,
)

for scan in results:
    print(scan.ip, scan.domain, scan.status)
    if scan.result is not None:
        print(scan.result)
```

Results are streamed. Iterating does not load the complete scan into memory.
The zgrab2 process starts when iteration begins.

## Targets

A string is interpreted as an IP address/CIDR when possible and otherwise as a domain:

```python
scanner.http(['192.0.2.1', '192.0.2.0/24', 'example.com'])
```

Use `Target` when you need the full zgrab2 input format:

```python
from pyzgrab2 import Target

target = Target(
    ip='192.0.2.10',
    domain='service.example.com',
    tag='production-web',
    port=8443,
)
```

A plain string is shorthand for a `Target`:

```python
'example.com'       # Equivalent to Target(domain='example.com')
'192.0.2.10'        # Equivalent to Target(ip='192.0.2.10')
'192.0.2.0/24'      # Equivalent to Target(ip='192.0.2.0/24')
```

Use the short string form for ordinary scans. Use `Target` when you need to
control how zgrab2 connects to or identifies a host:

- `ip` is the IP address zgrab2 connects to. It may also be a CIDR range.
- `domain` is resolved through DNS when no IP is supplied. When an IP is also
  supplied, the domain is still used in protocol-specific contexts such as the
  HTTP `Host` header and TLS SNI.
- `tag` labels the target and can be combined with zgrab2's `--trigger` option
  to select which tagged targets a scan applies to.
- `port` overrides the module's port for this individual target.

For example:

```python
Target(
    ip='203.0.113.10',
    domain='shop.example.com',
    tag='web',
    port=8443,
)
```

This tells zgrab2 to connect to `203.0.113.10:8443` while identifying the
server as `shop.example.com` for HTTP virtual hosting, TLS SNI, and related
protocol behavior. This is useful when a domain has multiple addresses or you
need to test a specific server without losing its hostname identity.

The fields correspond to zgrab2's `IP,DOMAIN,TAG,PORT` input format.

## HTTP

```python
from pyzgrab2 import HTTPOptions, ScanOptions, TLSOptions, ZGrab2

scanner = ZGrab2()

for scan in scanner.http(
    ['example.com'],
    options=ScanOptions(port=443, target_timeout='30s'),
    http=HTTPOptions(
        method='GET',
        endpoint='/health',
        user_agent='pyzgrab2/1.0',
        max_size=256,
        max_redirects=3,
        use_https=True,
        redirects_succeed=True,
        tls=TLSOptions(
            server_name='example.com',
            next_protocols=('h2', 'http/1.1'),
        ),
    ),
):
    print(scan.status, scan.result)
```

`HTTPOptions` supports common request, redirect, HTTP-version, HTTPS, and TLS
settings. Options not modeled by pyzgrab2 can be passed through `extra_args`.

## TLS

```python
from pyzgrab2 import ScanOptions, TLSOptions, ZGrab2

scanner = ZGrab2()

results = scanner.tls(
    ['example.com'],
    options=ScanOptions(port=443),
    tls=TLSOptions(
        server_name='example.com',
        handshake_timeout='15s',
        session_ticket=True,
        sct=True,
        keep_client_logs=True,
    ),
)

for scan in results:
    print(scan.status, scan.protocol, scan.port)
```

## SSH

```python
from pyzgrab2 import SSHOptions, ScanOptions, ZGrab2

scanner = ZGrab2()

results = scanner.ssh(
    ['192.0.2.20'],
    options=ScanOptions(port=22),
    ssh=SSHOptions(
        client='SSH-2.0-pyzgrab2',
        extensions=True,
        kex_algorithms=('curve25519-sha256',),
        host_key_algorithms=('ssh-ed25519',),
    ),
)

for scan in results:
    print(scan.status, scan.result)
```

Use `hello_only=True` when only the initial SSH identification exchange is
needed. zgrab2 does not allow `offer_unsupported=True` together with
`extensions=True` or `userauth=True`; pyzgrab2 validates this before starting
the process.

## IMAP

Use `starttls=True` for STARTTLS on the standard IMAP port, or `imaps=True`
for an immediate TLS connection, normally on port 993:

```python
from pyzgrab2 import IMAPOptions, ScanOptions, TLSOptions, ZGrab2

scanner = ZGrab2()

results = scanner.imap(
    ['mail.example.com'],
    options=ScanOptions(port=143),
    imap=IMAPOptions(
        starttls=True,
        send_close=True,
        tls=TLSOptions(server_name='mail.example.com'),
    ),
)

for scan in results:
    print(scan.status, scan.result)
```

`imaps` and `starttls` select different connection modes and cannot be enabled
together.

## FTP

Use `authtls=True` to request explicit TLS after connecting to FTP, or
`implicit_tls=True` to begin TLS immediately:

```python
from pyzgrab2 import FTPOptions, ScanOptions, TLSOptions, ZGrab2

scanner = ZGrab2()

results = scanner.ftp(
    ['ftp.example.com'],
    options=ScanOptions(port=21),
    ftp=FTPOptions(
        authtls=True,
        tls=TLSOptions(server_name='ftp.example.com'),
    ),
)

for scan in results:
    print(scan.status, scan.result)
```

## SMTP

SMTP supports banner and capability collection, optional STARTTLS, and
immediate TLS with `smtps=True`:

```python
from pyzgrab2 import SMTPOptions, ScanOptions, TLSOptions, ZGrab2

scanner = ZGrab2()

results = scanner.smtp(
    ['mail.example.com'],
    options=ScanOptions(port=25),
    smtp=SMTPOptions(
        send_help=True,
        send_quit=True,
        ehlo_domain='scanner.example.com',
        tls=TLSOptions(server_name='mail.example.com'),
    ),
)

for scan in results:
    print(scan.status, scan.result)
```

Use `send_starttls_override=True` to issue STARTTLS even when the server does
not advertise it, or `smtps=True` with the appropriate port for immediate TLS.
The HELO and EHLO override flags are also available for servers with unusual
SMTP capability behavior.

## Generic modules

Use `scan()` for banner, NTP, Redis, or any other module installed in the local zgrab2 binary:

```python
from pyzgrab2 import ScanOptions, ZGrab2

scanner = ZGrab2()

for scan in scanner.scan(
    ['192.0.2.30'],
    module='banner',
    options=ScanOptions(port=12345, target_timeout='20s'),
    module_args=['--max-size', '1024'],
):
    print(scan.status, scan.result)
```

Arguments in `module_args` are passed directly to zgrab2. Check the installed
binary for supported flags:

```sh
zgrab2 MODULE --help
```

The same help text is available from Python:

```python
print(scanner.module_help("banner"))
```

## Common scan options

`ScanOptions` covers frequently used zgrab2 controls:

- default port and per-target connection/scan timeouts
- sender count and connections per host
- read limits and per-server rate limits
- custom DNS resolvers and DNS timeout
- IPv4/IPv6 domain resolution
- debug, flush, verbose, and tag-trigger flags
- `extra_args` for application flags not modeled by the wrapper

For example:

```python
options = ScanOptions(
    senders=500,
    connections_per_host=1,
    connect_timeout='5s',
    target_timeout='30s',
    dns_resolvers=('1.1.1.1:53', '8.8.8.8:53'),
    server_rate_limit=20,
    resolve_ipv4=True,
)
```

The `timeout` parameter on `scan()`, `http()`, `tls()`, and `ssh()` is a Python
wall-clock limit for the entire zgrab2 subprocess. It is separate from
zgrab2's per-connection and per-target timeout flags.

## Results

Each output line becomes a `ScanResult` with:

- `ip`, `domain`, and `tag`
- `module` and `status`
- `data`: the complete common/module envelope under `data[module]`
- `result`: the module-specific result mapping, when present
- `protocol`, `port`, `timestamp`, and `error` convenience properties
- `raw`: the complete decoded zgrab2 output object

Module-specific results intentionally remain mappings in v1 because each
zgrab2 protocol has its own evolving schema.

```python
if scan.status == 'success':
    module_data = scan.result
else:
    print(scan.error)

print(scan.raw)
```

## Errors

All package exceptions derive from `ZGrab2Error`:

```python
from pyzgrab2 import (
    ZGrab2,
    ZGrab2NotFoundError,
    ZGrab2OutputError,
    ZGrab2ProcessError,
    ZGrab2TimeoutError,
)

try:
    results = list(ZGrab2().tls(['example.com'], timeout=60))
except ZGrab2NotFoundError:
    print('Install zgrab2 or provide its path')
except ZGrab2TimeoutError:
    print('The complete scan exceeded its wall-clock limit')
except ZGrab2ProcessError as error:
    print(error.returncode, error.stderr, error.command)
except ZGrab2OutputError as error:
    print(f'Unexpected zgrab2 output: {error}')
```

`ZGrab2ProcessError` exposes `command`, `returncode`, and `stderr` for
programmatic diagnostics.

## License

pyzgrab2 is distributed under the GNU General Public License v3.0. See [LICENSE](LICENSE).
