Metadata-Version: 2.4
Name: cyver-reporting
Version: 0.0.8
Summary: A Python client and wrapper around the Cyver API.
Author-email: Henrique Soares <henrique.soares@thoropass.com>
License: MIT License
        
        Copyright (c) 2026 Thoropass Inc.
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/thoropass/cyver-client
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography
Requires-Dist: platformdirs
Requires-Dist: PyJWT
Requires-Dist: requests
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# 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(project_id, 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, finding search across the full pentester
surface, 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

<!-- BEGIN api-coverage (auto-generated by tests/tools/regenerate_api_coverage.py) -->
The library covers approximately **56 of 76** Cyver API V2.2 endpoints (~74%).
The remaining endpoints are tracked under their respective items in ROADMAP1 (write-side resource creates, file imports, report downloads, full client-side CRUD).

| Section | Implemented | Total |
|---------|:-----------:|:-----:|
| Authentication | 3 | 3 |
| Clients | 8 | 8 |
| Users | 5 | 5 |
| Teams | 3 | 5 |
| Projects (Pentester) | 12 | 13 |
| Continuous Projects (Pentester) | 10 | 13 |
| Findings | 5 | 6 |
| Reference Data | 6 | 6 |
| Projects (Client) | 2 | 4 |
| Continuous Projects (Client) | 2 | 4 |
| Findings (Client) | 0 | 1 |
| Assets (Client) | 0 | 4 |
| Users (Client) | 0 | 4 |
<!-- END api-coverage -->

### 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`), finding-template listing
(`get_finding_templates`), full-surface finding search
(`search_findings` / `search_all_findings`), and a unique-code
generator (`create_unique_finding_code`) that computes the next free
`F-YYYY-N` (or `TP-YYYY-N`) identifier with an on-disk cache
fast-path.

`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, finding search, unique-code helper)
├── 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         # …, _re_cyver_code (matches F-YYYY-N and TP-YYYY-N finding codes)
│   ├── 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_optional_uuid_alias, _fd_nested_list, _parse_cyver_code, … (used by every from_dict)
│   ├── RequestHelpers.py        # _add_indexed_list (ABP-style indexed-array query expander, used by search_findings)
│   ├── FieldCaching.py          # FindingCodeCache (on-disk cache used by create_unique_finding_code)
│   ├── 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
└── tools/                       # Maintenance scripts (not part of the SDK package)
    ├── audit_trigger_events.py     # Postman-vs-source audit of `?triggerEvents=` wiring
    └── regenerate_api_coverage.py  # Regenerates the README API Coverage table from Postman
```

---

## 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.
