Metadata-Version: 2.5
Name: pyzgrab2
Version: 1.0.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.

The first release provides convenient option models for HTTP, TLS, and SSH.
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.

## Generic modules

Use `scan()` for banner, SMTP, 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.

## Publishing releases with GitLab CI/CD

The repository pipeline tests Python 3.10 through 3.13, builds and validates the
wheel and source distribution, and publishes version tags to public PyPI using
Trusted Publishing. No long-lived PyPI API token is stored in GitLab.

Before the first release, sign in to [PyPI](https://pypi.org/), open your account
**Publishing** page, and add a pending GitLab trusted publisher with:

- PyPI project name: `pyzgrab2`
- GitLab namespace: `dgiakatos`
- GitLab project: `pyzgrab2`
- Top-level pipeline file: `.gitlab-ci.yml`
- Environment: `pypi`

Then commit and push the pipeline. To publish the version declared in
`pyproject.toml`, create and push the matching `v`-prefixed tag:

```console
git tag -s v1.0.0 -m "pyzgrab2 1.0.0"
git push origin v1.0.0
```

The pipeline refuses to publish when the tag does not match the built package
version. PyPI does not allow replacing an existing release, so increment the
version before publishing another build.

## License

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