Metadata-Version: 2.4
Name: cyver-reporting
Version: 0.0.4
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.
- **Portal-surface classes** (`PentesterPortal` / `ClientPortal`) — opt-in subclasses that add wrappers for the **undocumented frontend API** the Cyver web UI exercises (project-file management, finding libraries, finding templates, etc.). Inherit every documented V2.2 method from their parent session class, plus the additional portal methods. Static API keys are rejected at construction — only password-authenticated access tokens are accepted by this surface.
- **Data builders** — `CyverClient`, `CyverAsset`, `CyverUser`, `CyverProject`, `CyverFinding`, `CyverFindingEvidence`, `CyverFindingCustomField`, `CyverFindingLibrary`, `CyverProjectFile`, plus read-only template classes (`CyverProjectTemplate`, `CyverReportTemplate`, `CyverChecklistTemplate`, `CyverComplianceTemplate`, `CyverReportTemplateSection`) and supporting classes (`CyverClientInformation`, `CyverClientAddress`, `CyverProjectDates`, `CyverPlanningDate`) — all provide a validated, fluent interface for constructing API request bodies, catching type and format errors before any request is dispatched.
- **Typed return values** — every read and write method returns the matching `Cyver*` data-builder instance instead of a raw dict. `get_clients()` / `get_all_clients()` / `get_client_by_id()` / `create_client()` / `update_client_by_id()` return `CyverClient`. The `get_assets_by_client_id()` family and `create_client_asset()` / `update_client_asset_by_id()` return `CyverAsset`. `get_users()` / `get_all_users()` / `get_user_by_id()` / `create_user()` / `update_user_by_id()` return `CyverUser`. `PentesterSession.get_projects()` / `get_all_projects()` / `get_project_by_id()` / `create_project()` and `ClientSession.get_projects()` / `get_all_projects()` / `get_project_by_id()` return `CyverProject`. `get_findings()` / `get_all_findings()` / `get_finding_by_id()` / `create_finding()` / `update_finding_by_id()` return `CyverFinding`. Project-/report-/checklist-/compliance-norm-template getters return the corresponding `CyverProjectTemplate` / `CyverReportTemplate` / `CyverChecklistTemplate` / `CyverComplianceTemplate`. The portal-surface methods follow the same convention: `get_project_images_files()` / `get_project_files()` return `CyverProjectFile`; `get_finding_libraries()` returns `CyverFindingLibrary`; `get_finding_templates()` returns `CyverFinding` instances hydrated in template mode. 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. **Opt out** at construction with `cache_dir=None`, redirect to a custom directory with `cache_dir=Path(...)`, or set the `CYVER_REPORTING_CACHE_DIR` environment variable globally — see [Cache Configuration](docs/authentication.md#cache-configuration).
- **Robust cache failure handling** — corrupted, undecryptable, or unwritable cache files are caught at every entry point that touches the cache (`authenticate()`, token refresh, the API-key constructor flow), logged as a `WARNING`, and the session falls back to fresh portal probe + re-authentication. The library never breaks because the cache is damaged. Callers that want explicit handling can catch `CyverCachingError`.
- **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
```

### Portal surface (undocumented frontend API)

`PentesterPortal` and `ClientPortal` add wrappers around the frontend-only
endpoints the Cyver web UI exercises — project-file management, finding
libraries, finding templates, and other operations the documented V2.2 API
does not expose. They subclass `PentesterSession` / `ClientSession`, so the
full V2.2 method surface is available unchanged; the additional methods are
decorated with `@portal_unstable` and emit a `CyverPortalInstabilityWarning`
on first call.

The portal API rejects static API keys, so portal classes raise
`CyverAuthMethodError` at construction if `api_key=` is supplied. Use
password authentication instead.

```python
from CyverReporting import PentesterPortal, PentesterSession
from CyverReporting.Utils import _finding_library

# Fetch finding templates from a library and create project findings
# from them — the canonical "fetch template → modify → create" workflow.
with PentesterPortal() as portal, PentesterSession() as pentester:
    portal.authenticate("app.cyver.io", "user@example.com", "s3cr3t")
    pentester.authenticate("app.cyver.io", "user@example.com", "s3cr3t")

    for template in portal.get_all_finding_templates(
        finding_library_id="1556a4fe-bee8-440a-9151-c746b27a5c0b",
        filter_status=_finding_library.published,
    ):
        # template.id is None — server assigns a new UUID on create.
        template.set_recommendation("Custom recommendation for this engagement.")
        pentester.create_finding(my_project_id, template)
```

See [docs/portal.md](docs/portal.md) for the full admitted-method list,
the endpoint-admission protocol, and the stability disclaimer.

### Disabling the credential cache

Pass `cache_dir=None` (or set `CYVER_REPORTING_CACHE_DIR=` empty) for
ephemeral runtimes — Lambda, ECS, CI runners, read-only filesystems, or
privacy-sensitive environments where tokens must never touch disk. The
portal probe then runs on every authentication, and warm-start
optimisations are skipped:

```python
from CyverReporting import PentesterSession

with PentesterSession(cache_dir=None) as client:  # no on-disk caching
    client.authenticate("app.cyver.io", "user@example.com", "s3cr3t")
    ...
```

```bash
# Or via env var, no code change:
CYVER_REPORTING_CACHE_DIR= python my_script.py
```

See [docs/authentication.md](docs/authentication.md#cache-configuration) for
the full kwarg/env-var matrix and the failure-handling contract.

---

## 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 |
| [Portal surface (undocumented)](docs/portal.md) | `PentesterPortal` / `ClientPortal` — opt-in access to the Cyver web frontend API |
| [Exceptions](docs/exceptions.md) | Exception hierarchy and error handling guide |

---

## API Coverage

### Documented V2.2 surface

The library covers approximately **55 of 69** Cyver API V2.2 endpoints (~80%).
The remaining endpoints are write-heavy operations (some resource creates,
full updates, finding imports, 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) | 11 | 16 |
| Continuous Projects (Pentester) | 11 | 14 |
| Findings | 6 | 7 |
| Reference Data | 6 | 6 |
| Projects (Client) | 2 | 2 |
| Continuous Projects (Client) | 2 | 2 |

### Portal surface (undocumented)

`PentesterPortal` exposes wrappers around frontend-only endpoints the Cyver
web UI exercises but the V2.2 API does not document. Each method is
admitted under the protocol described in [docs/portal.md](docs/portal.md).
The currently admitted set covers project-file management
(`get_project_images_files`, `get_project_files`, `update_project_file`,
`delete_project_file` — plus the corresponding `get_all_*` iterators),
finding-library CRUD (`get_finding_libraries`, `create_finding_library`,
`delete_finding_library`), and finding-template listing
(`get_finding_templates`).

`ClientPortal` is currently a skeleton (no portal-specific methods admitted
yet); it inherits the full `ClientSession` surface unchanged.

---

## Project Structure

```
cyver-reporting/                 # source dir — imported as `CyverReporting`
├── __init__.py                  # Public exports: session classes, portal classes, exceptions, data builders, alias constants
├── Session.py                   # Base session: authentication, portal probe, token cache (with cache_dir + CYVER_REPORTING_CACHE_DIR), HTTP transport
├── PentesterSession.py          # Pentester-role V2.2 API methods
├── ClientSession.py             # Client-role V2.2 API methods
├── PentesterPortal.py           # PentesterSession + admitted frontend-only methods (project files, finding libraries, finding templates)
├── ClientPortal.py              # ClientSession + frontend-only methods (skeleton; no methods admitted yet)
├── DataTypes/                   # Data-builder classes for read+write API request/response bodies
│   ├── __init__.py
│   ├── CyverClient.py           # write-side builder (paired with CyverClientInformation, CyverClientAddress)
│   ├── CyverClientInformation.py
│   ├── CyverClientAddress.py
│   ├── CyverAsset.py            # write-side builder for client assets
│   ├── CyverUser.py             # write-side builder for users
│   ├── CyverProject.py          # write-side builder for projects (paired with CyverProjectDates, CyverPlanningDate)
│   ├── CyverProjectDates.py
│   ├── CyverPlanningDate.py
│   ├── CyverFinding.py          # write-side builder for findings (also hydrated in template-mode by get_finding_templates)
│   ├── CyverFindingEvidence.py
│   ├── CyverFindingCustomField.py
│   ├── CyverProjectFile.py      # portal-surface project-file builder (consumed by PentesterPortal)
│   ├── CyverFindingLibrary.py   # portal-surface finding-library builder (consumed by PentesterPortal)
│   ├── CyverProjectTemplate.py     # read-only reference templates
│   ├── CyverChecklistTemplate.py
│   ├── CyverComplianceTemplate.py
│   ├── CyverReportTemplate.py
│   └── CyverReportTemplateSection.py
├── Utils/                       # Private helpers, compiled regexes, format validators, static constant classes
│   ├── __init__.py
│   ├── RegExPatterns.py
│   ├── StaticAliases.py         # _asset_type, _client_status, _project_status, _finding_severity, _finding_library, _project_file_type, …
│   ├── FormatValidators.py      # _validate_uuid, _validate_finding_severity, _validate_project_file_type, _validate_finding_library, …
│   ├── FieldExtractors.py       # _fd_optional_str, _fd_optional_uuid, _fd_nested_list, … (used by every from_dict)
│   ├── MimeTypes.py             # _guess_mime_type — used by multipart upload helpers
│   └── WarningHandlers.py       # CyverPortalInstabilityWarning + portal_unstable decorator
├── Exceptions/                  # Full exception hierarchy
│   ├── __init__.py
│   ├── Base.py                  # CyverError, CyverReferenceError, CyverCachingError
│   ├── Authentication.py        # CyverAuthError, CyverAuthMethodError, Cyver2FARequired
│   ├── Connection.py            # CyverHTTPError, CyverNotFoundError, CyverPermissionError, CyverRateLimitError
│   └── Validation.py            # CyverDataValidationError
└── 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_portal.py               # Portal skeletons + portal_unstable decorator + admission allow-list
├── test_portal_*.py             # Per-method portal tests (project files, finding libraries, finding templates)
├── test_validators_client.py    # CyverClient / CyverClientInformation / CyverClientAddress tests
├── test_validators_user.py      # CyverUser tests
├── test_finding.py              # CyverFinding tests (incl. template-mode hydration)
├── test_finding_evidence.py     # CyverFindingEvidence tests
├── test_finding_custom_field.py # CyverFindingCustomField tests
├── test_finding_library.py      # CyverFindingLibrary tests
├── test_project_file.py         # CyverProjectFile tests
├── test_cache_disable.py        # cache_dir + CYVER_REPORTING_CACHE_DIR + CyverCachingError
├── 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,
    CyverAuthMethodError,
    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 CyverAuthMethodError:
    # Raised at construction by PentesterPortal / ClientPortal when an
    # `api_key=` was supplied — the portal API rejects static keys.
    print("Portal classes require password authentication.")
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}")
```

`CyverCachingError` (cache I/O / decrypt failures) is **caught
internally** at every entry point that touches the cache and degrades
the session to a fresh portal probe + re-authentication, with a
`WARNING` log. Callers don't need to handle it directly unless they
invoke the private cache I/O methods themselves. See
[docs/exceptions.md](docs/exceptions.md) for the full exception
hierarchy and reason-code reference.

---

## License

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