A Python client and wrapper around the Cyver API.
Project description
cyver-reporting
A Python client library for the Cyver Pentest Management API. Developed and maintained by penetration testers at Thoropass.
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()returnCyverClient. Theget_assets_by_client_id()family andcreate_client_asset()/update_client_asset_by_id()returnCyverAsset.get_users()/get_all_users()/get_user_by_id()/create_user()/update_user_by_id()returnCyverUser.PentesterSession.get_projects()/get_all_projects()/get_project_by_id()/create_project()andClientSession.get_projects()/get_all_projects()/get_project_by_id()returnCyverProject.get_findings()/get_all_findings()/get_finding_by_id()/create_finding()/update_finding_by_id()returnCyverFinding. Project-/report-/checklist-/compliance-norm-template getters return the correspondingCyverProjectTemplate/CyverReportTemplate/CyverChecklistTemplate/CyverComplianceTemplate. The portal-surface methods follow the same convention:get_project_images_files()/get_project_files()returnCyverProjectFile;get_finding_libraries()returnsCyverFindingLibrary;get_finding_templates()returnsCyverFindinginstances hydrated in template mode. Theidproperty 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 withcache_dir=Path(...), or set theCYVER_REPORTING_CACHE_DIRenvironment variable globally — see 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 aWARNING, 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 catchCyverCachingError. - 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
withblocks to guarantee the underlying HTTP session is always closed. - Type annotations — fully annotated and ships with a
py.typedmarker (PEP 561).
Requirements
- Python 3.10 or later
- A valid Cyver portal account with Pentester or Client role
Installation
pip install cyver-reporting
Quick Start
Pentester — username/password
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:
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
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
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:
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:
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:
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:
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.
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 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:
from CyverReporting import PentesterSession
with PentesterSession(cache_dir=None) as client: # no on-disk caching
client.authenticate("app.cyver.io", "user@example.com", "s3cr3t")
...
# Or via env var, no code change:
CYVER_REPORTING_CACHE_DIR= python my_script.py
See docs/authentication.md for the full kwarg/env-var matrix and the failure-handling contract.
Documentation
| Document | Description |
|---|---|
| Authentication | Login, API key auth, portal probe, 2FA, token caching, and session lifecycle |
| Pentester API | All PentesterSession methods and parameters |
| Client API | All ClientSession methods and parameters |
| Portal surface (undocumented) | PentesterPortal / ClientPortal — opt-in access to the Cyver web frontend API |
| Exceptions | Exception hierarchy and error handling guide |
API Coverage
Documented V2.2 surface
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 |
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.
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
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 for the full exception
hierarchy and reason-code reference.
License
MIT — see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cyver_reporting-0.0.7.tar.gz.
File metadata
- Download URL: cyver_reporting-0.0.7.tar.gz
- Upload date:
- Size: 245.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
878a792f6b4c2ab1c5331ac17554d32415bd6a73e03b7f21d6e884a4ff318b01
|
|
| MD5 |
fcd937ec809a8acfd8837ed39888e313
|
|
| BLAKE2b-256 |
ba09bceb90931460c3275bed1c9308728482c5e256b57d9af67ebae365f68d08
|
File details
Details for the file cyver_reporting-0.0.7-py3-none-any.whl.
File metadata
- Download URL: cyver_reporting-0.0.7-py3-none-any.whl
- Upload date:
- Size: 183.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
acc1cc1f1b949be92d9ea91fa41671f3b07b801c82230aabb8bab6b9fb823852
|
|
| MD5 |
6da73da77052d1b733897a037033e330
|
|
| BLAKE2b-256 |
72cc28a6c5af345c2679d16f7090e8bd06bb4e29663f631fb93ec72e33af38e4
|