Metadata-Version: 2.4
Name: cyver-reporting
Version: 0.0.3
Summary: A Python client and wrapper around the Cyver API.
Author-email: Henrique Soares <henrique.soares@thoropass.com>
License: MIT
Project-URL: Homepage, https://github.com/thoropass/cyver-client
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: cryptography
Requires-Dist: platformdirs
Requires-Dist: PyJWT
Requires-Dist: requests
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"

# cyver-reporting

A Python client library for the [Cyver](https://cyver.io) Pentest Management API.
Developed and maintained by penetration testers at [Thoropass](https://thoropass.com).

---

## Features

- **Two authentication modes** — username/password login with email-based 2FA and automatic token refresh, or stateless static API key via `X-API-Key`.
- **Portal validation** — availability probe before credentials are ever sent; raises a clear error if the portal is inactive or unreachable. Skipped automatically when a local cache confirms the portal was active in a previous run.
- **Pentester role** (`PentesterSession`) — manage clients, assets, users, teams, projects, continuous projects, findings, and reference data.
- **Client role** (`ClientSession`) — read projects and continuous projects scoped to the authenticated client user.
- **Data builders** — `CyverClient`, `CyverAsset`, `CyverFinding`, `CyverFindingEvidence`, `CyverFindingCustomField`, and companion classes provide a validated, fluent interface for constructing API request bodies, catching type and format errors before any request is dispatched.
- **Typed return values** — `get_clients()`, `get_all_clients()`, `get_client_by_id()`, `create_client()`, `update_client_by_id()`, `get_assets_by_client_id()`, `get_all_assets_by_client_id()`, `create_client_asset()`, and `update_client_asset_by_id()` all return `CyverClient` / `CyverAsset` instances. `get_users()`, `get_all_users()`, and `get_user_by_id()` return `CyverUser` instances. `create_finding()` and `update_finding_by_id()` return `CyverFinding` instances. The `id` property on each object carries the server-assigned UUID.
- **Transparent pagination** — every list endpoint ships with a companion `get_all_*()` generator that pages through all results automatically.
- **Secure token caching** — OAuth tokens are encrypted with a PBKDF2-derived Fernet key and stored in the OS user-cache directory with owner-only file permissions. The cached portal hostname enables probe-skip on warm starts and stale cache cleanup on failures. API key sessions maintain a separate lightweight cache for the same portal-validation purpose.
- **Resilient HTTP transport** — automatic retries with exponential back-off on transient server errors and rate-limiting responses.
- **Thread-safe** — all token state is protected by a reentrant lock, safe for use from multiple threads.
- **Context manager support** — use `with` blocks to guarantee the underlying HTTP session is always closed.
- **Type annotations** — fully annotated and ships with a `py.typed` marker (PEP 561).

---

## Requirements

- Python 3.10 or later
- A valid Cyver portal account with Pentester or Client role

---

## Installation

```bash
pip install cyver-reporting
```

---

## Quick Start

### Pentester — username/password

```python
from CyverReporting import PentesterSession, _project_status

with PentesterSession() as client:
    client.authenticate("app.cyver.io", "user@example.com", "s3cr3t")

    # Iterate over every active project without managing pagination manually
    for project in client.get_all_projects(filter_status=_project_status.testing):
        print(project.name)

    # Fetch all critical and high findings for a specific project
    for finding in client.get_all_findings(
        project_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        severity_list=["Critical", "High"],
    ):
        print(finding.name, finding.severity)
```

### Pentester — static API key

Pass `portal` and `api_key` to the constructor. The session is immediately
ready — no call to `authenticate()` is needed or allowed:

```python
from CyverReporting import PentesterSession, _project_status

with PentesterSession(portal="app.cyver.io", api_key="sk-...") as client:
    for project in client.get_all_projects(filter_status=_project_status.testing):
        print(project.name)
```

### Client — username/password

```python
from CyverReporting import ClientSession

with ClientSession() as client:
    client.authenticate("app.cyver.io", "user@example.com", "s3cr3t")

    for project in client.get_all_projects():
        print(project.name)
```

### Client — static API key

```python
from CyverReporting import ClientSession

with ClientSession(portal="app.cyver.io", api_key="sk-...") as client:
    for project in client.get_all_projects():
        print(project.name)
```

### Two-Factor Authentication

When 2FA is enabled on the account, a verification code is sent to the
registered e-mail address and a `Cyver2FARequired` is raised on the first call:

```python
from CyverReporting import PentesterSession
from CyverReporting.Exceptions import Cyver2FARequired

client = PentesterSession()

try:
    client.authenticate("app.cyver.io", "user@example.com", "s3cr3t")
except Cyver2FARequired:
    code = input("Enter the 2FA code sent to your email: ")
    client.authenticate("app.cyver.io", "user@example.com", "s3cr3t",
                        verification_code=code)
```

### Creating a Client (data builder)

Use `CyverClient` to construct and validate the request body before calling
`create_client()`. All setter methods return `self` for fluent chaining:

```python
from CyverReporting import PentesterSession, CyverClient, CyverClientInformation, CyverClientAddress

with PentesterSession(portal="app.cyver.io", api_key="sk-...") as pentester:
    new_client = (
        CyverClient("Acme Corp")
        .set_status(1)
        .set_client_number("CLI-0042")
        .set_client_information(
            CyverClientInformation()
            .set_company_name("Acme Corporation")
            .set_website("https://acme.example.com")
            .set_address(
                CyverClientAddress()
                .set_street("123 Main St")
                .set_city("San Francisco")
                .set_country("United States")
            )
        )
    )
    created = pentester.create_client(new_client)
    print(created.id)  # server-assigned UUID
```

### Creating a Client Asset (data builder)

Use `CyverAsset` to construct and validate an asset request body. Each asset
type imposes additional required fields (e.g. `url` for Web Application, `ip`
for Network) that are enforced before the request is dispatched:

```python
from CyverReporting import PentesterSession, CyverAsset

with PentesterSession(portal="app.cyver.io", api_key="sk-...") as pentester:
    asset = (
        CyverAsset("Main Portal")
        .set_url("https://portal.example.com")
        .set_type(2)           # Web Application — also validates url is set
        .set_environment(3)    # Production
        .set_hosting_type(1)   # Public Cloud (Azure)
        .set_public_facing(2)  # External
    )
    created = pentester.create_client_asset(client_id, asset)
    print(created.id)  # server-assigned asset UUID
```

### Creating a Finding (data builder)

Use `CyverFinding` to construct and validate a finding request body. `name` is
the only required field; all other fields are optional:

```python
from CyverReporting import PentesterSession, CyverFinding, _finding_severity, _finding_status

with PentesterSession(portal="app.cyver.io", api_key="sk-...") as pentester:
    finding = (
        CyverFinding("SQL Injection in Login Form")
        .set_severity(_finding_severity.high)
        .set_status(_finding_status.draft)
        .set_cvss31_vector("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")
        .set_cvss31_score(9.8)
        .add_cwe("CWE-89")
    )
    created = pentester.create_finding(finding)
    print(created.id)  # server-assigned UUID
```

---

## Documentation

| Document | Description |
|----------|-------------|
| [Authentication](docs/authentication.md) | Login, API key auth, portal probe, 2FA, token caching, and session lifecycle |
| [Pentester API](docs/pentester.md) | All `PentesterSession` methods and parameters |
| [Client API](docs/client.md) | All `ClientSession` methods and parameters |
| [Exceptions](docs/exceptions.md) | Exception hierarchy and error handling guide |

---

## API Coverage

The library covers **54 of 69** Cyver API V2.2 endpoints (78%). The remaining
endpoints are write-heavy operations (resource creation, full updates, file
uploads, report downloads) whose exact request body schemas require additional
information beyond what the public API specification provides.

| Section | Implemented | Total |
|---------|:-----------:|:-----:|
| Authentication | 3 | 3 |
| Clients | 8 | 9 |
| Users | 3 | 5 |
| Teams | 3 | 5 |
| Projects (Pentester) | 10 | 16 |
| Continuous Projects (Pentester) | 11 | 14 |
| Findings | 6 | 7 |
| Reference Data | 6 | 6 |
| Projects (Client) | 2 | 2 |
| Continuous Projects (Client) | 2 | 2 |

---

## Project Structure

```
CyverReporting/
├── __init__.py               # Public exports: CyverSession, PentesterSession, ClientSession, exceptions, data builders, constants
├── Session.py                # Base session: authentication, portal probe, token cache, HTTP transport
├── PentesterSession.py       # PentesterSession — pentester-role API methods
├── ClientSession.py          # ClientSession — client-role API methods
├── DataTypes/                # Cyver* data-builder classes (CyverClient, CyverAsset, CyverUser, …)
│   ├── __init__.py
│   ├── CyverClient.py
│   ├── CyverAsset.py
│   ├── CyverUser.py
│   ├── CyverFinding.py
│   ├── CyverFindingEvidence.py
│   ├── CyverFindingCustomField.py
│   └── …
├── Utils/                    # Private helpers, compiled regexes, format validators, and CY_* static constant classes
│   ├── __init__.py
│   ├── RegExPatterns.py
│   ├── StaticAliases.py
│   ├── FormatValidators.py
│   └── FieldExtractors.py
├── Exceptions/               # Full exception hierarchy (CyverError, CyverAuthError, CyverDataValidationError, …)
│   ├── __init__.py
│   ├── Base.py
│   ├── Authentication.py
│   ├── Connection.py
│   └── Validation.py
└── py.typed                  # PEP 561 marker
tests/
├── conftest.py               # Shared fixtures and helpers
├── test_session.py           # Session, authentication, portal probe tests
├── test_client.py            # ClientSession tests
├── test_pentester.py         # PentesterSession tests
├── test_validators_client.py # CyverClient / CyverClientInformation / CyverClientAddress tests
├── test_validators_user.py   # CyverUser tests
├── test_finding_evidence.py  # CyverFindingEvidence tests
├── test_finding_custom_field.py # CyverFindingCustomField tests
├── test_finding.py           # CyverFinding tests
├── test_exceptions.py        # Exception hierarchy tests
└── test_utils.py             # Format validator tests
```

---

## Error Handling

```python
from CyverReporting import PentesterSession, _project_status
from CyverReporting.Exceptions import (
    Cyver2FARequired,
    CyverAuthError,
    CyverDataValidationError,
    CyverHTTPError,
)

client = PentesterSession()

try:
    client.authenticate(portal, username, password)
except Cyver2FARequired:
    # 2FA code required — re-call authenticate() with verification_code=
    code = input("Enter the 2FA code sent to your email: ")
    client.authenticate(portal, username, password, verification_code=code)
except CyverAuthError as e:
    if e.reason == "portal_inactive":
        print(f"Portal '{portal}' does not exist or is not active.")
    elif e.reason == "portal_unreachable":
        print(f"Portal '{portal}' could not be reached. Check connectivity.")
    else:
        print(f"Authentication failed: {e}")
except CyverDataValidationError as e:
    # Invalid argument passed to a method or data builder (wrong type, bad UUID, etc.)
    print(f"Data error on field '{e.field}' ({e.reason}): {e}")
except CyverHTTPError as e:
    # Network failure, non-200 status code, or malformed JSON response
    print(f"HTTP error: {e}")
```

---

## License

MIT — see [LICENSE](LICENSE) for details.
